Skip to main content

sozu_command_lib/
request.rs

1use std::{
2    error,
3    fmt::{self, Display},
4    fs::File,
5    io::{BufReader, Read},
6    net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
7    str::FromStr,
8};
9
10use prost::{DecodeError, Message};
11use rusty_ulid::Ulid;
12
13use crate::{
14    proto::{
15        command::{
16            AddBackend, ClientAuthMode, ClientAuthPolicy, HttpsListenerConfig, InitialState,
17            IpAddress, LoadBalancingAlgorithms, PathRuleKind, RedirectPolicy, Request,
18            RequestHttpFrontend, RulePosition, SocketAddress, Uint128, UpdateHttpsListenerConfig,
19            WorkerRequest, ip_address, request::RequestType,
20        },
21        display::format_request_type,
22    },
23    response::HttpFrontend,
24    state::ClusterId,
25};
26
27#[derive(thiserror::Error, Debug)]
28pub enum RequestError {
29    #[error("invalid value {value} for field '{name}'")]
30    InvalidValue { name: String, value: i32 },
31    #[error("Could not read requests from file: {0}")]
32    ReadFile(std::io::Error),
33    #[error("Could not decode requests: {0}")]
34    Decode(DecodeError),
35}
36
37impl Request {
38    /// determine to which of the four proxies (HTTP, HTTPS, TCP, UDP) a request is destined
39    pub fn get_destinations(&self) -> ProxyDestinations {
40        let mut proxy_destination = ProxyDestinations {
41            to_http_proxy: false,
42            to_https_proxy: false,
43            to_tcp_proxy: false,
44            to_udp_proxy: false,
45        };
46        let request_type = match &self.request_type {
47            Some(t) => t,
48            None => return proxy_destination,
49        };
50
51        match request_type {
52            RequestType::AddHttpFrontend(_)
53            | RequestType::AddHttpFrontendWithSilentClose(_)
54            | RequestType::RemoveHttpFrontend(_)
55            | RequestType::AddHttpFrontendWithHeaderPredicates(_)
56            | RequestType::RemoveHttpFrontendWithHeaderPredicates(_) => {
57                proxy_destination.to_http_proxy = true
58            }
59
60            RequestType::AddHttpsFrontend(_)
61            | RequestType::AddHttpsFrontendWithSilentClose(_)
62            | RequestType::RemoveHttpsFrontend(_)
63            | RequestType::AddHttpsFrontendWithHeaderPredicates(_)
64            | RequestType::RemoveHttpsFrontendWithHeaderPredicates(_)
65            | RequestType::AddCertificate(_)
66            | RequestType::QueryCertificatesFromWorkers(_)
67            | RequestType::ReplaceCertificate(_)
68            | RequestType::RemoveCertificate(_) => proxy_destination.to_https_proxy = true,
69
70            RequestType::AddTcpFrontend(_) | RequestType::RemoveTcpFrontend(_) => {
71                proxy_destination.to_tcp_proxy = true
72            }
73
74            RequestType::AddUdpFrontend(_) | RequestType::RemoveUdpFrontend(_) => {
75                proxy_destination.to_udp_proxy = true
76            }
77
78            RequestType::AddCluster(_)
79            | RequestType::AddBackend(_)
80            | RequestType::AddUnixBackend(_)
81            | RequestType::RemoveCluster(_)
82            | RequestType::RemoveBackend(_)
83            | RequestType::SetHealthCheck(_)
84            | RequestType::SetProtectedHealthCheck(_)
85            | RequestType::RemoveHealthCheck(_)
86            | RequestType::SoftStop(_)
87            | RequestType::HardStop(_)
88            | RequestType::Status(_) => {
89                proxy_destination.to_http_proxy = true;
90                proxy_destination.to_https_proxy = true;
91                proxy_destination.to_tcp_proxy = true;
92                proxy_destination.to_udp_proxy = true;
93            }
94
95            // handled at worker level prior to this call
96            RequestType::ConfigureMetrics(_)
97            | RequestType::SetMetricDetail(_)
98            | RequestType::QueryMetrics(_)
99            | RequestType::Logging(_)
100            | RequestType::QueryClustersHashes(_)
101            | RequestType::QueryClusterById(_)
102            | RequestType::QueryClustersByDomain(_)
103            | RequestType::SetMaxConnectionsPerIp(_)
104            | RequestType::QueryMaxConnectionsPerIp(_)
105            | RequestType::SetMaxConnectionsPerSubnet(_)
106            | RequestType::QueryMaxConnectionsPerSubnet(_)
107            | RequestType::SetMaxConnectionAttempts(_)
108            | RequestType::CloseSessions(_)
109            // handled by the worker's protected health check protocol before
110            // this call, which only a proxy-bound order reaches
111            | RequestType::QueryProtectedHealthCheckSupport(_)
112            | RequestType::ProtectedHealthCheckOrder(_) => {}
113
114            // the Add***Listener / Update***Listener and other Listener orders will be
115            // handled separately by the notify_proxys function, so we don't give them
116            // destinations here
117            RequestType::AddHttpsListener(_)
118            | RequestType::AddHttpsListenerWithClientAuth(_)
119            | RequestType::AddHttpListener(_)
120            | RequestType::AddTcpListener(_)
121            | RequestType::AddUdpListener(_)
122            | RequestType::UpdateHttpListener(_)
123            | RequestType::UpdateHttpsListener(_)
124            | RequestType::UpdateHttpsListenerWithClientAuth(_)
125            | RequestType::UpdateTcpListener(_)
126            | RequestType::UpdateUdpListener(_)
127            | RequestType::RemoveListener(_)
128            | RequestType::ActivateListener(_)
129            | RequestType::DeactivateListener(_)
130            | RequestType::ReturnListenSockets(_) => {}
131
132            // These won't ever reach a worker anyway
133            RequestType::SaveState(_)
134            | RequestType::CountRequests(_)
135            | RequestType::QueryCertificatesFromTheState(_)
136            | RequestType::QueryHealthChecks(_)
137            | RequestType::LoadState(_)
138            | RequestType::ListWorkers(_)
139            | RequestType::ListFrontends(_)
140            | RequestType::ListListeners(_)
141            | RequestType::LaunchWorker(_)
142            | RequestType::UpgradeMain(_)
143            | RequestType::UpgradeWorker(_)
144            | RequestType::SubscribeEvents(_)
145            | RequestType::ReloadConfiguration(_) => {}
146        }
147
148        // POST: HTTP-frontend orders route to the HTTP proxy ONLY, HTTPS /
149        // certificate orders to the HTTPS proxy ONLY, and TCP-frontend orders
150        // to the TCP proxy ONLY — a frontend order must never fan out across
151        // protocol planes (that would double-apply the order). Cluster-wide
152        // and broadcast orders (AddCluster, SoftStop, …) legitimately target
153        // all three, so we only assert the single-plane exclusivity here.
154        debug_assert!(
155            !(proxy_destination.to_http_proxy
156                && proxy_destination.to_https_proxy
157                && proxy_destination.to_tcp_proxy)
158                || matches!(
159                    self.request_type,
160                    Some(
161                        RequestType::AddCluster(_)
162                            | RequestType::AddBackend(_)
163                            | RequestType::AddUnixBackend(_)
164                            | RequestType::RemoveCluster(_)
165                            | RequestType::RemoveBackend(_)
166                            | RequestType::SetHealthCheck(_)
167                            | RequestType::SetProtectedHealthCheck(_)
168                            | RequestType::RemoveHealthCheck(_)
169                            | RequestType::SoftStop(_)
170                            | RequestType::HardStop(_)
171                            | RequestType::Status(_)
172                    )
173                ),
174            "only cluster-wide / broadcast orders may target all three proxy planes"
175        );
176        // POST: a None request_type carries no destination at all.
177        debug_assert!(
178            self.request_type.is_some()
179                || (!proxy_destination.to_http_proxy
180                    && !proxy_destination.to_https_proxy
181                    && !proxy_destination.to_tcp_proxy),
182            "a request without a request_type must have no proxy destination"
183        );
184        proxy_destination
185    }
186
187    /// True if the request is a SoftStop or a HardStop
188    pub fn is_a_stop(&self) -> bool {
189        matches!(
190            self.request_type,
191            Some(RequestType::SoftStop(_)) | Some(RequestType::HardStop(_))
192        )
193    }
194
195    pub fn short_name(&self) -> &str {
196        match &self.request_type {
197            Some(request_type) => format_request_type(request_type),
198            None => "Unallowed",
199        }
200    }
201}
202
203impl WorkerRequest {
204    pub fn new(id: String, content: Request) -> Self {
205        Self { id, content }
206    }
207}
208
209impl HttpsListenerConfig {
210    /// True when the listener asks for a client certificate.
211    ///
212    /// Any value other than `CLIENT_AUTH_NONE` counts, including one this
213    /// build does not know: it must travel on the verb a pre-mTLS peer
214    /// cannot decode, and the worker then rejects it when it builds the
215    /// listener.
216    pub fn requests_client_auth(&self) -> bool {
217        self.client_auth
218            .is_some_and(|mode| mode != ClientAuthMode::ClientAuthNone as i32)
219    }
220
221    /// The listener's client authentication policy, as a patch would carry it.
222    pub fn client_auth_policy(&self) -> ClientAuthPolicy {
223        ClientAuthPolicy {
224            client_auth: self.client_auth,
225            client_ca_certificates: self.client_ca_certificates.clone(),
226            client_ca_crls: self.client_ca_crls.clone(),
227        }
228    }
229
230    /// Replace the client authentication policy with `policy`, all three
231    /// fields at once: a CA or CRL the policy leaves out is dropped. The main
232    /// process and the workers both apply a patch through this, so their
233    /// copies of the listener cannot diverge.
234    pub fn apply_client_auth_policy(&mut self, policy: &ClientAuthPolicy) {
235        self.client_auth = policy.client_auth;
236        self.client_ca_certificates = policy.client_ca_certificates.clone();
237        self.client_ca_crls = policy.client_ca_crls.clone();
238    }
239}
240
241impl RequestType {
242    /// The request that adds this HTTPS listener, on the verb its client
243    /// authentication policy calls for.
244    ///
245    /// A listener that asks for a client certificate goes out as
246    /// `AddHttpsListenerWithClientAuth`, which a worker or main process that
247    /// predates mutual TLS cannot decode, so it fails the request instead of
248    /// building the listener without client authentication. Any other
249    /// listener keeps `AddHttpsListener`, which every version understands.
250    pub fn add_https_listener(listener: HttpsListenerConfig) -> Self {
251        if listener.requests_client_auth() {
252            RequestType::AddHttpsListenerWithClientAuth(listener)
253        } else {
254            RequestType::AddHttpsListener(listener)
255        }
256    }
257
258    /// The request that applies this HTTPS listener patch: a patch that
259    /// replaces the client authentication policy goes out as
260    /// `UpdateHttpsListenerWithClientAuth`, which a worker that predates the
261    /// policy field cannot decode, so it fails the patch instead of keeping
262    /// the CA and CRLs it had. Any other patch keeps `UpdateHttpsListener`.
263    pub fn update_https_listener(patch: UpdateHttpsListenerConfig) -> Self {
264        if patch.client_auth_policy.is_some() {
265            RequestType::UpdateHttpsListenerWithClientAuth(patch)
266        } else {
267            RequestType::UpdateHttpsListener(patch)
268        }
269    }
270
271    /// The request that adds this backend, on the verb its endpoint calls for.
272    ///
273    /// A backend with a `unix_path` goes out as `AddUnixBackend`, which a
274    /// worker or main process that predates UNIX backends cannot decode: it
275    /// sees no known request, so it never registers the backend under its
276    /// identity `address` nor dials that over TCP, and it sends no response.
277    /// Any other backend keeps `AddBackend`, which every version understands.
278    pub fn add_backend(backend: AddBackend) -> Self {
279        if backend.unix_path.is_some() {
280            RequestType::AddUnixBackend(backend)
281        } else {
282            RequestType::AddBackend(backend)
283        }
284    }
285
286    /// The same request, with a backend addition, an HTTPS listener addition
287    /// or patch, or an HTTP(S) frontend addition or removal moved to the verb
288    /// [`RequestType::add_backend`], [`RequestType::add_https_listener`],
289    /// [`RequestType::update_https_listener`],
290    /// [`RequestType::add_http_frontend`],
291    /// [`RequestType::remove_http_frontend`],
292    /// [`RequestType::add_https_frontend`] or
293    /// [`RequestType::remove_https_frontend`] picks for it. Every other
294    /// request is returned unchanged.
295    ///
296    /// The main process applies this to every request it fans out to the
297    /// workers, so a client that sends client authentication or a `unix_path`
298    /// on a historical verb cannot get it silently dropped by a worker that
299    /// predates it.
300    pub fn into_canonical(self) -> Self {
301        match self {
302            RequestType::AddBackend(backend) | RequestType::AddUnixBackend(backend) => {
303                RequestType::add_backend(backend)
304            }
305            RequestType::AddHttpsListener(listener)
306            | RequestType::AddHttpsListenerWithClientAuth(listener) => {
307                RequestType::add_https_listener(listener)
308            }
309            RequestType::UpdateHttpsListener(patch)
310            | RequestType::UpdateHttpsListenerWithClientAuth(patch) => {
311                RequestType::update_https_listener(patch)
312            }
313            RequestType::AddHttpFrontend(front)
314            | RequestType::AddHttpFrontendWithSilentClose(front)
315            | RequestType::AddHttpFrontendWithHeaderPredicates(front) => {
316                RequestType::add_http_frontend(front)
317            }
318            RequestType::RemoveHttpFrontend(front)
319            | RequestType::RemoveHttpFrontendWithHeaderPredicates(front) => {
320                RequestType::remove_http_frontend(front)
321            }
322            RequestType::AddHttpsFrontend(front)
323            | RequestType::AddHttpsFrontendWithSilentClose(front)
324            | RequestType::AddHttpsFrontendWithHeaderPredicates(front) => {
325                RequestType::add_https_frontend(front)
326            }
327            RequestType::RemoveHttpsFrontend(front)
328            | RequestType::RemoveHttpsFrontendWithHeaderPredicates(front) => {
329                RequestType::remove_https_frontend(front)
330            }
331            other => other,
332        }
333    }
334
335    /// The request that adds this HTTP frontend, on the verb it calls for.
336    ///
337    /// A frontend with header predicates goes out as
338    /// `AddHttpFrontendWithHeaderPredicates`, which a worker that predates
339    /// header routing cannot decode, so it fails the request instead of
340    /// installing the frontend without its guard. Otherwise a frontend whose
341    /// redirect policy is `SILENT_CLOSE` goes out as
342    /// `AddHttpFrontendWithSilentClose`, which a worker that predates the
343    /// policy cannot decode, so it fails the request instead of resolving the
344    /// unknown policy to `FORWARD` and forwarding the traffic. Any other
345    /// frontend keeps `AddHttpFrontend`, which every version understands.
346    pub fn add_http_frontend(front: RequestHttpFrontend) -> Self {
347        if !front.header_predicates.is_empty() {
348            RequestType::AddHttpFrontendWithHeaderPredicates(front)
349        } else if front.redirect == Some(RedirectPolicy::SilentClose as i32) {
350            RequestType::AddHttpFrontendWithSilentClose(front)
351        } else {
352            RequestType::AddHttpFrontend(front)
353        }
354    }
355
356    /// As [`RequestType::add_http_frontend`], for the removal. An old worker
357    /// would otherwise remove the unpredicated sibling sharing the key.
358    pub fn remove_http_frontend(front: RequestHttpFrontend) -> Self {
359        if front.header_predicates.is_empty() {
360            RequestType::RemoveHttpFrontend(front)
361        } else {
362            RequestType::RemoveHttpFrontendWithHeaderPredicates(front)
363        }
364    }
365
366    /// As [`RequestType::add_http_frontend`], for an HTTPS frontend.
367    pub fn add_https_frontend(front: RequestHttpFrontend) -> Self {
368        if !front.header_predicates.is_empty() {
369            RequestType::AddHttpsFrontendWithHeaderPredicates(front)
370        } else if front.redirect == Some(RedirectPolicy::SilentClose as i32) {
371            RequestType::AddHttpsFrontendWithSilentClose(front)
372        } else {
373            RequestType::AddHttpsFrontend(front)
374        }
375    }
376
377    /// As [`RequestType::remove_http_frontend`], for an HTTPS frontend.
378    pub fn remove_https_frontend(front: RequestHttpFrontend) -> Self {
379        if front.header_predicates.is_empty() {
380            RequestType::RemoveHttpsFrontend(front)
381        } else {
382            RequestType::RemoveHttpsFrontendWithHeaderPredicates(front)
383        }
384    }
385}
386
387impl Request {
388    /// [`RequestType::into_canonical`] applied to the request type.
389    pub fn into_canonical(self) -> Self {
390        Request {
391            request_type: self.request_type.map(RequestType::into_canonical),
392        }
393    }
394}
395
396impl fmt::Display for WorkerRequest {
397    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
398        write!(f, "{}-{:?}", self.id, self.content)
399    }
400}
401
402pub fn read_initial_state_from_file(file: &mut File) -> Result<InitialState, RequestError> {
403    let mut buf_reader = BufReader::new(file);
404    read_initial_state(&mut buf_reader)
405}
406
407pub fn read_initial_state<R: Read>(reader: &mut R) -> Result<InitialState, RequestError> {
408    let mut buffer = Vec::new();
409    reader
410        .read_to_end(&mut buffer)
411        .map_err(RequestError::ReadFile)?;
412
413    InitialState::decode(&buffer[..]).map_err(RequestError::Decode)
414}
415
416#[derive(Debug, Clone, PartialEq, Eq, Hash)]
417pub struct ProxyDestinations {
418    pub to_http_proxy: bool,
419    pub to_https_proxy: bool,
420    pub to_tcp_proxy: bool,
421    pub to_udp_proxy: bool,
422}
423
424impl RequestHttpFrontend {
425    /// convert a requested frontend to a usable one by parsing its address
426    pub fn to_frontend(self) -> Result<HttpFrontend, RequestError> {
427        let requested_hostname = self.hostname.clone();
428        let requested_cluster_id = self.cluster_id.clone();
429        let frontend = HttpFrontend {
430            address: self.address.into(),
431            cluster_id: self.cluster_id.map(ClusterId::from),
432            hostname: self.hostname,
433            path: self.path,
434            method: self.method,
435            position: RulePosition::try_from(self.position).map_err(|_| {
436                RequestError::InvalidValue {
437                    name: "position".to_string(),
438                    value: self.position,
439                }
440            })?,
441            tags: Some(self.tags),
442            redirect: self.redirect,
443            redirect_scheme: self.redirect_scheme,
444            redirect_template: self.redirect_template,
445            rewrite_host: self.rewrite_host,
446            rewrite_path: self.rewrite_path,
447            rewrite_port: self.rewrite_port,
448            required_auth: self.required_auth,
449            headers: self.headers,
450            hsts: self.hsts,
451            header_predicates: self.header_predicates,
452        };
453
454        // POST: routing identity (hostname + cluster_id) is carried through
455        // unchanged — only the address is reparsed and the position is mapped
456        // through the proto enum. A frontend whose hostname or cluster shifted
457        // here would route traffic to the wrong place.
458        debug_assert_eq!(
459            frontend.hostname, requested_hostname,
460            "hostname must survive the frontend conversion"
461        );
462        debug_assert_eq!(
463            frontend.cluster_id.as_deref(),
464            requested_cluster_id.as_deref(),
465            "cluster_id must survive the frontend conversion"
466        );
467        Ok(frontend)
468    }
469}
470
471impl Display for RequestHttpFrontend {
472    /// Used to create a unique summary of the frontend, used as a key in maps
473    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
474        let s = match &PathRuleKind::try_from(self.path.kind) {
475            Ok(PathRuleKind::Prefix) => {
476                format!("{};{};P{}", self.address, self.hostname, self.path.value)
477            }
478            Ok(PathRuleKind::Regex) => {
479                format!("{};{};R{}", self.address, self.hostname, self.path.value)
480            }
481            Ok(PathRuleKind::Equals) => {
482                format!("{};{};={}", self.address, self.hostname, self.path.value)
483            }
484            // Its own key letter: a `REGEX` and a `REGEX_PATH` frontend with
485            // the same pattern route differently and must not collide in
486            // `ConfigState` (sozu-proxy/sozu#1909).
487            Ok(PathRuleKind::RegexPath) => {
488                format!("{};{};Q{}", self.address, self.hostname, self.path.value)
489            }
490            Err(e) => format!("Wrong variant of PathRuleKind: {e}"),
491        };
492
493        match &self.method {
494            Some(method) => write!(f, "{s};{method}")?,
495            None => write!(f, "{s}")?,
496        }
497
498        // Header predicates are part of the frontend's identity
499        // (sozu-proxy/sozu#1253): a canary frontend that differs from its
500        // base only by a predicate must not collide with it in
501        // `ConfigState`. Nothing is appended without predicates, so every
502        // pre-existing key is unchanged. Names compare case-insensitively
503        // and the list is a conjunction, so the key lowercases names and
504        // sorts and deduplicates entries, as the router does: two spellings of one rule share one key. Values
505        // are `Debug`-quoted so a `"` or `;` inside one cannot forge a
506        // boundary.
507        if !self.header_predicates.is_empty() {
508            let mut predicates: Vec<(String, i32, Option<&str>)> = self
509                .header_predicates
510                .iter()
511                .map(|predicate| {
512                    (
513                        predicate.name.to_ascii_lowercase(),
514                        predicate.kind,
515                        predicate.value.as_deref(),
516                    )
517                })
518                .collect();
519            predicates.sort();
520            predicates.dedup();
521            for (name, kind, value) in predicates {
522                write!(f, ";H{kind}:{name:?}")?;
523                if let Some(value) = value {
524                    write!(f, "={value:?}")?;
525                }
526            }
527        }
528        Ok(())
529    }
530}
531
532#[derive(Debug)]
533pub struct ParseErrorLoadBalancing;
534
535impl fmt::Display for ParseErrorLoadBalancing {
536    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
537        write!(f, "Cannot find the load balancing policy asked")
538    }
539}
540
541impl error::Error for ParseErrorLoadBalancing {
542    fn description(&self) -> &str {
543        "Cannot find the load balancing policy asked"
544    }
545
546    fn cause(&self) -> Option<&dyn error::Error> {
547        None
548    }
549}
550
551impl FromStr for LoadBalancingAlgorithms {
552    type Err = ParseErrorLoadBalancing;
553
554    fn from_str(s: &str) -> Result<Self, Self::Err> {
555        match s.to_lowercase().as_str() {
556            "round_robin" => Ok(LoadBalancingAlgorithms::RoundRobin),
557            "random" => Ok(LoadBalancingAlgorithms::Random),
558            "power_of_two" => Ok(LoadBalancingAlgorithms::PowerOfTwo),
559            "least_loaded" => Ok(LoadBalancingAlgorithms::LeastLoaded),
560            "hrw" => Ok(LoadBalancingAlgorithms::Hrw),
561            "maglev" => Ok(LoadBalancingAlgorithms::Maglev),
562            _ => Err(ParseErrorLoadBalancing {}),
563        }
564    }
565}
566
567impl SocketAddress {
568    pub fn new_v4(a: u8, b: u8, c: u8, d: u8, port: u16) -> Self {
569        SocketAddr::new(IpAddr::V4(Ipv4Addr::new(a, b, c, d)), port).into()
570    }
571}
572
573impl From<SocketAddr> for SocketAddress {
574    fn from(socket_addr: SocketAddr) -> SocketAddress {
575        let ip_inner = match socket_addr {
576            SocketAddr::V4(ip_v4_addr) => ip_address::Inner::V4(u32::from(*ip_v4_addr.ip())),
577            SocketAddr::V6(ip_v6_addr) => {
578                ip_address::Inner::V6(Uint128::from(u128::from(*ip_v6_addr.ip())))
579            }
580        };
581
582        let encoded = SocketAddress {
583            port: socket_addr.port() as u32,
584            ip: IpAddress {
585                inner: Some(ip_inner),
586            },
587        };
588
589        // POST: the port widens losslessly (u16 → u32) and the proto address
590        // family matches the source family — a V4 SocketAddr must never encode
591        // as a V6 inner and vice versa, or the reverse `From` would synthesize
592        // the wrong address.
593        debug_assert_eq!(
594            encoded.port,
595            socket_addr.port() as u32,
596            "port must round-trip losslessly into the proto"
597        );
598        debug_assert_eq!(
599            matches!(encoded.ip.inner, Some(ip_address::Inner::V4(_))),
600            socket_addr.is_ipv4(),
601            "proto IP family must match the source SocketAddr family"
602        );
603        encoded
604    }
605}
606
607impl From<SocketAddress> for SocketAddr {
608    fn from(socket_address: SocketAddress) -> Self {
609        // PRE: a wire-sourced proto port may exceed u16::MAX (16-bit on the
610        // wire is carried as a 32-bit field). This is peer/config input, so we
611        // narrow rather than panic; the debug_assert only guards our *own*
612        // encoders, which never emit an out-of-range port.
613        debug_assert!(
614            socket_address.port <= u16::MAX as u32,
615            "self-encoded proto port must fit in a u16"
616        );
617        let had_inner = socket_address.ip.inner.is_some();
618        let port = socket_address.port as u16;
619
620        let ip = match socket_address.ip.inner {
621            Some(inner) => match inner {
622                ip_address::Inner::V4(v4_value) => IpAddr::V4(Ipv4Addr::from(v4_value)),
623                ip_address::Inner::V6(v6_value) => IpAddr::V6(Ipv6Addr::from(u128::from(v6_value))),
624            },
625            None => IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), // should never happen
626        };
627
628        let decoded = SocketAddr::new(ip, port);
629        // POST: a self-encoded proto address always carries an inner IP, so the
630        // unspecified-V4 fallback is only ever reached on malformed peer input.
631        debug_assert!(
632            had_inner || decoded.ip() == IpAddr::V4(Ipv4Addr::UNSPECIFIED),
633            "missing inner IP must decode to the unspecified-V4 sentinel"
634        );
635        decoded
636    }
637}
638
639impl From<Uint128> for u128 {
640    fn from(value: Uint128) -> Self {
641        let combined = value.low as u128 | ((value.high as u128) << 64);
642        // POST: the two 64-bit halves occupy disjoint bit ranges, so the low
643        // half is recoverable as the bottom 64 bits and the high half as the
644        // top 64 bits — the pack is bijective.
645        debug_assert_eq!(
646            combined as u64, value.low,
647            "low half must be the bottom 64 bits"
648        );
649        debug_assert_eq!(
650            (combined >> 64) as u64,
651            value.high,
652            "high half must be the top 64 bits"
653        );
654        combined
655    }
656}
657
658impl From<u128> for Uint128 {
659    fn from(value: u128) -> Self {
660        let low = value as u64;
661        let high = (value >> 64) as u64;
662        let packed = Uint128 { low, high };
663        // POST: splitting then recombining reproduces the original u128 — the
664        // split-into-halves and join-from-halves operations are mutual
665        // inverses (no bit is lost or duplicated).
666        debug_assert_eq!(
667            u128::from(Uint128 {
668                low: packed.low,
669                high: packed.high
670            }),
671            value,
672            "u128 → Uint128 → u128 must round-trip"
673        );
674        packed
675    }
676}
677
678impl From<i128> for Uint128 {
679    fn from(value: i128) -> Self {
680        Uint128::from(value as u128)
681    }
682}
683
684impl From<Ulid> for Uint128 {
685    fn from(value: Ulid) -> Self {
686        let (low, high) = value.into();
687        let packed = Uint128 { low, high };
688        // POST: the (low, high) tuple is carried verbatim into the proto, so
689        // re-reading it reconstructs the same Ulid — the encoding loses no bits.
690        debug_assert_eq!(
691            Ulid::from((packed.low, packed.high)),
692            value,
693            "Ulid → Uint128 must preserve all 128 bits"
694        );
695        packed
696    }
697}
698
699impl From<Uint128> for Ulid {
700    fn from(value: Uint128) -> Self {
701        let Uint128 { low, high } = value;
702        let ulid = Ulid::from((low, high));
703        // POST: the decode is the exact inverse of the encode above — the same
704        // halves go back out, so Uint128 → Ulid → Uint128 round-trips.
705        debug_assert_eq!(
706            Uint128::from(ulid),
707            value,
708            "Uint128 → Ulid must preserve all 128 bits"
709        );
710        ulid
711    }
712}
713
714#[cfg(test)]
715mod tests {
716    use prost::Message;
717
718    use super::*;
719    use crate::proto::command::{HeaderMatchKind, HeaderPredicate, PathRule};
720
721    fn https_listener(client_auth: Option<i32>) -> HttpsListenerConfig {
722        HttpsListenerConfig {
723            address: SocketAddress::new_v4(127, 0, 0, 1, 8443),
724            client_auth,
725            client_ca_certificates: vec!["CLIENT CA PEM".to_owned()],
726            ..Default::default()
727        }
728    }
729
730    const NONE: i32 = ClientAuthMode::ClientAuthNone as i32;
731    const OPTIONAL: i32 = ClientAuthMode::ClientAuthOptional as i32;
732    const REQUIRED: i32 = ClientAuthMode::ClientAuthRequired as i32;
733
734    #[test]
735    fn add_https_listener_picks_the_verb_from_the_client_auth_mode() {
736        for client_auth in [None, Some(NONE)] {
737            assert!(
738                matches!(
739                    RequestType::add_https_listener(https_listener(client_auth)),
740                    RequestType::AddHttpsListener(_)
741                ),
742                "a listener without client auth ({client_auth:?}) keeps the verb every worker decodes"
743            );
744        }
745        // 999 names no mode: it must not travel on the verb an old worker
746        // would decode and silently strip, the worker rejects it at build.
747        for client_auth in [Some(OPTIONAL), Some(REQUIRED), Some(999)] {
748            assert!(
749                matches!(
750                    RequestType::add_https_listener(https_listener(client_auth)),
751                    RequestType::AddHttpsListenerWithClientAuth(_)
752                ),
753                "a listener asking for a client certificate ({client_auth:?}) needs the mTLS verb"
754            );
755        }
756    }
757
758    /// A worker that predates `RedirectPolicy::SilentClose` resolves the
759    /// unknown enum value to `Forward` and would forward the traffic, so a
760    /// silent-close frontend must leave on a verb such a worker cannot decode.
761    #[test]
762    fn silent_close_frontends_travel_on_their_own_verb() {
763        let front = |redirect: Option<RedirectPolicy>| RequestHttpFrontend {
764            hostname: "example.com".to_owned(),
765            redirect: redirect.map(|policy| policy as i32),
766            ..Default::default()
767        };
768        let silent = front(Some(RedirectPolicy::SilentClose));
769        assert_eq!(
770            RequestType::AddHttpFrontend(silent.clone()).into_canonical(),
771            RequestType::AddHttpFrontendWithSilentClose(silent.clone()),
772            "silent close sent on the historical verb must be moved to its own verb"
773        );
774        assert_eq!(
775            RequestType::AddHttpsFrontend(silent.clone()).into_canonical(),
776            RequestType::AddHttpsFrontendWithSilentClose(silent.clone()),
777        );
778        for policy in [
779            None,
780            Some(RedirectPolicy::Forward),
781            Some(RedirectPolicy::Unauthorized),
782        ] {
783            let plain = front(policy);
784            assert_eq!(
785                RequestType::AddHttpFrontendWithSilentClose(plain.clone()).into_canonical(),
786                RequestType::AddHttpFrontend(plain.clone()),
787                "a frontend that is not silent close keeps the verb every worker decodes"
788            );
789            assert_eq!(
790                RequestType::AddHttpsFrontendWithSilentClose(plain.clone()).into_canonical(),
791                RequestType::AddHttpsFrontend(plain),
792            );
793        }
794    }
795
796    /// The silent-close verbs sit on tags 70 and 71. Tags 66-69 belong to
797    /// the header-predicate verbs and 72 to `AddUnixBackend`, and a
798    /// worker that predates these tags sees an unknown `oneof` member, so the
799    /// request carries no route there instead of a forwarding one.
800    #[test]
801    fn silent_close_verbs_use_their_allocated_wire_tags() {
802        // A 2.3.1 worker drops the silent-close verbs as unknown members and
803        // keeps decoding the historical verbs of a non-silent frontend.
804        let silent = RequestHttpFrontend {
805            hostname: "example.com".to_owned(),
806            redirect: Some(RedirectPolicy::SilentClose as i32),
807            ..Default::default()
808        };
809        assert_eq!(
810            decode_on_a_legacy_worker(RequestType::add_http_frontend(silent.clone())),
811            None
812        );
813        assert_eq!(
814            decode_on_a_legacy_worker(RequestType::add_https_frontend(silent.clone())),
815            None
816        );
817        let plain = RequestHttpFrontend {
818            redirect: Some(RedirectPolicy::Forward as i32),
819            ..silent
820        };
821        assert_eq!(
822            decode_on_a_legacy_worker(RequestType::add_http_frontend(plain.clone())),
823            Some(LegacyRequestType::AddHttpFrontend(plain.clone()))
824        );
825        assert_eq!(
826            decode_on_a_legacy_worker(RequestType::add_https_frontend(plain.clone())),
827            Some(LegacyRequestType::AddHttpsFrontend(plain))
828        );
829
830        let front = RequestHttpFrontend {
831            hostname: "example.com".to_owned(),
832            redirect: Some(RedirectPolicy::SilentClose as i32),
833            ..Default::default()
834        };
835        let encode = |request_type| {
836            Request {
837                request_type: Some(request_type),
838            }
839            .encode_to_vec()
840        };
841        // key = (tag << 3) | length-delimited(2), as a varint
842        assert_eq!(
843            &encode(RequestType::AddHttpFrontendWithSilentClose(front.clone()))[..2],
844            &[0xB2, 0x04],
845            "tag 70"
846        );
847        assert_eq!(
848            &encode(RequestType::AddHttpsFrontendWithSilentClose(front.clone()))[..2],
849            &[0xBA, 0x04],
850            "tag 71"
851        );
852        // the legacy verbs keep their historical tags
853        let legacy = encode(RequestType::AddHttpFrontend(front));
854        assert_ne!(&legacy[..2], &[0xB2, 0x04]);
855    }
856
857    #[test]
858    fn into_canonical_moves_https_listeners_to_their_verb_both_ways() {
859        let required = https_listener(Some(REQUIRED));
860        assert_eq!(
861            RequestType::AddHttpsListener(required.clone()).into_canonical(),
862            RequestType::AddHttpsListenerWithClientAuth(required.clone()),
863            "client auth sent on the historical verb must be moved to the mTLS verb"
864        );
865        assert_eq!(
866            RequestType::AddHttpsListenerWithClientAuth(required.clone()).into_canonical(),
867            RequestType::AddHttpsListenerWithClientAuth(required),
868        );
869
870        let none = https_listener(None);
871        assert_eq!(
872            RequestType::AddHttpsListenerWithClientAuth(none.clone()).into_canonical(),
873            RequestType::AddHttpsListener(none.clone()),
874            "a listener without client auth goes back to the verb every worker decodes"
875        );
876        assert_eq!(
877            Request::from(RequestType::AddHttpsListener(none.clone())).into_canonical(),
878            Request::from(RequestType::AddHttpsListener(none)),
879        );
880
881        let other = RequestType::SoftStop(Default::default());
882        assert_eq!(other.clone().into_canonical(), other);
883        assert_eq!(
884            Request { request_type: None }.into_canonical(),
885            Request { request_type: None }
886        );
887    }
888
889    /// The `WorkerRequest` schema of a worker that predates mutual TLS, cut
890    /// down to what matters here: the envelope, and the one verb such a
891    /// worker had for adding an HTTPS listener. prost skips every tag a
892    /// schema does not declare, exactly as it does for the verbs elided here.
893    #[derive(Clone, PartialEq, Message)]
894    struct LegacyWorkerRequest {
895        #[prost(string, tag = "1")]
896        id: String,
897        #[prost(message, optional, tag = "2")]
898        content: Option<LegacyRequest>,
899    }
900
901    #[derive(Clone, PartialEq, Message)]
902    struct LegacyRequest {
903        #[prost(oneof = "LegacyRequestType", tags = "15, 17, 27, 48")]
904        request_type: Option<LegacyRequestType>,
905    }
906
907    #[derive(Clone, PartialEq, prost::Oneof)]
908    enum LegacyRequestType {
909        /// `add_http_frontend`, understood by every worker.
910        #[prost(message, tag = "15")]
911        AddHttpFrontend(RequestHttpFrontend),
912        /// `add_https_frontend`, understood by every worker.
913        #[prost(message, tag = "17")]
914        AddHttpsFrontend(RequestHttpFrontend),
915        /// `add_https_listener`, holding an `HttpsListenerConfig` that has no
916        /// client authentication field.
917        #[prost(message, tag = "27")]
918        AddHttpsListener(LegacyHttpsListenerConfig),
919        /// `update_https_listener`, holding an `UpdateHttpsListenerConfig`
920        /// that has no client authentication policy field.
921        #[prost(message, tag = "48")]
922        UpdateHttpsListener(LegacyHttpsListenerConfig),
923    }
924
925    #[derive(Clone, PartialEq, Message)]
926    struct LegacyHttpsListenerConfig {
927        #[prost(message, optional, tag = "1")]
928        address: Option<SocketAddress>,
929    }
930
931    fn decode_on_a_legacy_worker(request: RequestType) -> Option<LegacyRequestType> {
932        let bytes = WorkerRequest::new("ID-1".to_owned(), request.into()).encode_to_vec();
933        let legacy = LegacyWorkerRequest::decode(bytes.as_slice())
934            .expect("a legacy worker decodes the envelope of every request");
935        assert_eq!(legacy.id, "ID-1");
936        legacy
937            .content
938            .expect("the envelope always carries a request")
939            .request_type
940    }
941
942    /// A main process that knows mutual TLS talking to a worker that does
943    /// not, the state between `UpgradeMain` and the replacement of the last
944    /// old worker.
945    #[test]
946    fn a_worker_that_predates_mtls_cannot_decode_a_listener_that_requires_it() {
947        let required = https_listener(Some(REQUIRED));
948
949        // The failure mode the dedicated verb exists for: on the historical
950        // verb, the old worker sees a plain HTTPS listener, client auth gone.
951        assert!(
952            matches!(
953                decode_on_a_legacy_worker(RequestType::AddHttpsListener(required.clone())),
954                Some(LegacyRequestType::AddHttpsListener(_))
955            ),
956            "precondition: the historical verb decodes on an old worker without its client auth"
957        );
958
959        // What the main process actually sends: nothing the old worker can
960        // build a listener from, so it fails the request instead.
961        assert_eq!(
962            decode_on_a_legacy_worker(
963                RequestType::AddHttpsListener(required.clone()).into_canonical()
964            ),
965            None,
966            "a listener that requires a client certificate must be undecodable by an old worker"
967        );
968        assert_eq!(
969            decode_on_a_legacy_worker(RequestType::add_https_listener(required)),
970            None
971        );
972
973        // Positive space: a listener without client auth stays readable, so
974        // a mixed fleet keeps serving every listener it can serve safely.
975        assert_eq!(
976            decode_on_a_legacy_worker(RequestType::add_https_listener(https_listener(None))),
977            Some(LegacyRequestType::AddHttpsListener(
978                LegacyHttpsListenerConfig {
979                    address: Some(SocketAddress::new_v4(127, 0, 0, 1, 8443)),
980                }
981            ))
982        );
983    }
984
985    fn policy_patch(policy: Option<ClientAuthPolicy>) -> UpdateHttpsListenerConfig {
986        UpdateHttpsListenerConfig {
987            address: SocketAddress::new_v4(127, 0, 0, 1, 8443),
988            client_auth_policy: policy,
989            ..Default::default()
990        }
991    }
992
993    #[test]
994    fn update_https_listener_picks_the_verb_from_the_policy_field() {
995        assert!(matches!(
996            RequestType::update_https_listener(policy_patch(None)),
997            RequestType::UpdateHttpsListener(_)
998        ));
999        // Any policy, even one turning client auth off: a worker that ignored
1000        // it would keep enforcing, or keep trusting, what the patch replaces.
1001        for client_auth in [None, Some(NONE), Some(REQUIRED)] {
1002            let policy = ClientAuthPolicy {
1003                client_auth,
1004                ..Default::default()
1005            };
1006            assert!(
1007                matches!(
1008                    RequestType::update_https_listener(policy_patch(Some(policy))),
1009                    RequestType::UpdateHttpsListenerWithClientAuth(_)
1010                ),
1011                "a patch carrying a policy ({client_auth:?}) needs the mTLS verb"
1012            );
1013        }
1014        let with_policy = policy_patch(Some(ClientAuthPolicy::default()));
1015        assert_eq!(
1016            RequestType::UpdateHttpsListener(with_policy.clone()).into_canonical(),
1017            RequestType::UpdateHttpsListenerWithClientAuth(with_policy)
1018        );
1019        assert_eq!(
1020            RequestType::UpdateHttpsListenerWithClientAuth(policy_patch(None)).into_canonical(),
1021            RequestType::UpdateHttpsListener(policy_patch(None))
1022        );
1023    }
1024
1025    #[test]
1026    fn a_worker_that_predates_mtls_cannot_decode_a_client_auth_policy_patch() {
1027        let refresh = policy_patch(Some(https_listener(Some(REQUIRED)).client_auth_policy()));
1028        assert!(
1029            matches!(
1030                decode_on_a_legacy_worker(RequestType::UpdateHttpsListener(refresh.clone())),
1031                Some(LegacyRequestType::UpdateHttpsListener(_))
1032            ),
1033            "precondition: the historical verb decodes on an old worker without its policy"
1034        );
1035        assert_eq!(
1036            decode_on_a_legacy_worker(RequestType::update_https_listener(refresh)),
1037            None,
1038            "a patch replacing the client auth policy must be undecodable by an old worker"
1039        );
1040        assert!(matches!(
1041            decode_on_a_legacy_worker(RequestType::update_https_listener(policy_patch(None))),
1042            Some(LegacyRequestType::UpdateHttpsListener(_))
1043        ));
1044    }
1045
1046    #[test]
1047    fn applying_a_client_auth_policy_replaces_all_of_it() {
1048        let mut listener = https_listener(Some(REQUIRED));
1049        listener.client_ca_crls = vec!["OLD CRL".to_owned()];
1050        let policy = ClientAuthPolicy {
1051            client_auth: Some(OPTIONAL),
1052            client_ca_certificates: vec!["NEW CA".to_owned()],
1053            client_ca_crls: vec![],
1054        };
1055        listener.apply_client_auth_policy(&policy);
1056        assert_eq!(listener.client_auth_policy(), policy);
1057        assert!(
1058            listener.client_ca_crls.is_empty(),
1059            "a CRL the policy leaves out is dropped, not kept"
1060        );
1061    }
1062
1063    /// `WorkerRequest` of a worker that predates header routing: the four
1064    /// historical frontend verbs, holding a `RequestHttpFrontend` that has no
1065    /// `header_predicates` field.
1066    #[derive(Clone, PartialEq, Message)]
1067    struct LegacyFrontendWorkerRequest {
1068        #[prost(string, tag = "1")]
1069        id: String,
1070        #[prost(message, optional, tag = "2")]
1071        content: Option<LegacyFrontendRequest>,
1072    }
1073
1074    #[derive(Clone, PartialEq, Message)]
1075    struct LegacyFrontendRequest {
1076        #[prost(oneof = "LegacyFrontendRequestType", tags = "15, 16, 17, 18")]
1077        request_type: Option<LegacyFrontendRequestType>,
1078    }
1079
1080    #[derive(Clone, PartialEq, prost::Oneof)]
1081    enum LegacyFrontendRequestType {
1082        #[prost(message, tag = "15")]
1083        AddHttp(LegacyHttpFrontend),
1084        #[prost(message, tag = "16")]
1085        RemoveHttp(LegacyHttpFrontend),
1086        #[prost(message, tag = "17")]
1087        AddHttps(LegacyHttpFrontend),
1088        #[prost(message, tag = "18")]
1089        RemoveHttps(LegacyHttpFrontend),
1090    }
1091
1092    #[derive(Clone, PartialEq, Message)]
1093    struct LegacyHttpFrontend {
1094        #[prost(string, tag = "3")]
1095        hostname: String,
1096    }
1097
1098    fn decode_frontend_on_a_legacy_worker(
1099        request: RequestType,
1100    ) -> Option<LegacyFrontendRequestType> {
1101        let bytes = WorkerRequest::new("ID-1".to_owned(), request.into()).encode_to_vec();
1102        LegacyFrontendWorkerRequest::decode(bytes.as_slice())
1103            .expect("a legacy worker decodes the envelope of every request")
1104            .content
1105            .expect("the envelope always carries a request")
1106            .request_type
1107    }
1108
1109    /// A `REGEX` and a `REGEX_PATH` frontend on one pattern route differently,
1110    /// so their `ConfigState` keys must differ (sozu-proxy/sozu#1909).
1111    #[test]
1112    fn a_path_only_regex_frontend_keys_apart_from_the_same_regex() {
1113        let with_path = |path: PathRule| RequestHttpFrontend {
1114            path,
1115            ..frontend(false)
1116        };
1117        let regex = with_path(PathRule::regex("/a.*")).to_string();
1118        let regex_path = with_path(PathRule::regex_path("/a.*")).to_string();
1119        assert_ne!(regex, regex_path);
1120        assert_eq!(regex_path, "127.0.0.1:8080;example.com;Q/a.*");
1121    }
1122
1123    fn frontend(with_predicate: bool) -> RequestHttpFrontend {
1124        RequestHttpFrontend {
1125            cluster_id: Some("canary".to_owned()),
1126            address: SocketAddress::new_v4(127, 0, 0, 1, 8080),
1127            hostname: "example.com".to_owned(),
1128            header_predicates: if with_predicate {
1129                vec![HeaderPredicate {
1130                    name: "X-Canary".to_owned(),
1131                    kind: HeaderMatchKind::Present as i32,
1132                    value: None,
1133                }]
1134            } else {
1135                Vec::new()
1136            },
1137            ..Default::default()
1138        }
1139    }
1140
1141    #[test]
1142    fn frontend_verbs_follow_the_header_predicates() {
1143        let guarded = frontend(true);
1144        let plain = frontend(false);
1145        assert!(matches!(
1146            RequestType::add_http_frontend(guarded.clone()),
1147            RequestType::AddHttpFrontendWithHeaderPredicates(_)
1148        ));
1149        assert!(matches!(
1150            RequestType::remove_http_frontend(guarded.clone()),
1151            RequestType::RemoveHttpFrontendWithHeaderPredicates(_)
1152        ));
1153        assert!(matches!(
1154            RequestType::add_https_frontend(guarded.clone()),
1155            RequestType::AddHttpsFrontendWithHeaderPredicates(_)
1156        ));
1157        assert!(matches!(
1158            RequestType::remove_https_frontend(guarded.clone()),
1159            RequestType::RemoveHttpsFrontendWithHeaderPredicates(_)
1160        ));
1161        // routes without predicates keep the historical verbs
1162        assert_eq!(
1163            RequestType::add_http_frontend(plain.clone()),
1164            RequestType::AddHttpFrontend(plain.clone())
1165        );
1166        assert_eq!(
1167            RequestType::remove_https_frontend(plain.clone()),
1168            RequestType::RemoveHttpsFrontend(plain.clone())
1169        );
1170        // whatever verb the client used, the fan-out sends the canonical one
1171        assert_eq!(
1172            RequestType::AddHttpFrontend(guarded.clone()).into_canonical(),
1173            RequestType::AddHttpFrontendWithHeaderPredicates(guarded.clone())
1174        );
1175        assert_eq!(
1176            RequestType::RemoveHttpsFrontendWithHeaderPredicates(plain.clone()).into_canonical(),
1177            RequestType::RemoveHttpsFrontend(plain)
1178        );
1179    }
1180
1181    #[test]
1182    fn a_worker_that_predates_header_routing_cannot_decode_a_guarded_frontend() {
1183        let guarded = frontend(true);
1184        // The failure mode the dedicated verbs exist for: on a historical
1185        // verb the old worker installs the route and drops its guard.
1186        assert!(
1187            matches!(
1188                decode_frontend_on_a_legacy_worker(RequestType::AddHttpFrontend(guarded.clone())),
1189                Some(LegacyFrontendRequestType::AddHttp(_))
1190            ),
1191            "precondition: the historical verb decodes on an old worker without its guard"
1192        );
1193        for request in [
1194            RequestType::add_http_frontend(guarded.clone()),
1195            RequestType::remove_http_frontend(guarded.clone()),
1196            RequestType::add_https_frontend(guarded.clone()),
1197            RequestType::remove_https_frontend(guarded.clone()),
1198            RequestType::AddHttpsFrontend(guarded.clone()).into_canonical(),
1199            RequestType::RemoveHttpFrontend(guarded.clone()).into_canonical(),
1200        ] {
1201            assert_eq!(
1202                decode_frontend_on_a_legacy_worker(request),
1203                None,
1204                "a guarded frontend must be undecodable by an old worker"
1205            );
1206        }
1207        // Positive space: unguarded frontends stay readable in a mixed fleet.
1208        assert_eq!(
1209            decode_frontend_on_a_legacy_worker(RequestType::add_http_frontend(frontend(false))),
1210            Some(LegacyFrontendRequestType::AddHttp(LegacyHttpFrontend {
1211                hostname: "example.com".to_owned()
1212            }))
1213        );
1214    }
1215
1216    /// The tags are wire contract: 65 belongs to `close_sessions`, 70-72 to
1217    /// other pending verbs, so a drift here collides with them.
1218    #[test]
1219    fn header_predicate_verbs_use_the_allocated_wire_tags() {
1220        let guarded = frontend(true);
1221        for (request, tag) in [
1222            (RequestType::add_http_frontend(guarded.clone()), 66u32),
1223            (RequestType::remove_http_frontend(guarded.clone()), 67),
1224            (RequestType::add_https_frontend(guarded.clone()), 68),
1225            (RequestType::remove_https_frontend(guarded.clone()), 69),
1226        ] {
1227            let bytes = Request::from(request).encode_to_vec();
1228            // first byte is the key of the oneof field: (tag << 3) | length-delimited
1229            let (key, _) = prost::encoding::decode_varint(&mut bytes.as_slice())
1230                .map(|k| (k, ()))
1231                .expect("a request starts with a field key");
1232            assert_eq!(key >> 3, u64::from(tag));
1233            assert_eq!(key & 7, 2);
1234        }
1235    }
1236}