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            Err(e) => format!("Wrong variant of PathRuleKind: {e}"),
485        };
486
487        match &self.method {
488            Some(method) => write!(f, "{s};{method}")?,
489            None => write!(f, "{s}")?,
490        }
491
492        // Header predicates are part of the frontend's identity
493        // (sozu-proxy/sozu#1253): a canary frontend that differs from its
494        // base only by a predicate must not collide with it in
495        // `ConfigState`. Nothing is appended without predicates, so every
496        // pre-existing key is unchanged. Names compare case-insensitively
497        // and the list is a conjunction, so the key lowercases names and
498        // sorts and deduplicates entries, as the router does: two spellings of one rule share one key. Values
499        // are `Debug`-quoted so a `"` or `;` inside one cannot forge a
500        // boundary.
501        if !self.header_predicates.is_empty() {
502            let mut predicates: Vec<(String, i32, Option<&str>)> = self
503                .header_predicates
504                .iter()
505                .map(|predicate| {
506                    (
507                        predicate.name.to_ascii_lowercase(),
508                        predicate.kind,
509                        predicate.value.as_deref(),
510                    )
511                })
512                .collect();
513            predicates.sort();
514            predicates.dedup();
515            for (name, kind, value) in predicates {
516                write!(f, ";H{kind}:{name:?}")?;
517                if let Some(value) = value {
518                    write!(f, "={value:?}")?;
519                }
520            }
521        }
522        Ok(())
523    }
524}
525
526#[derive(Debug)]
527pub struct ParseErrorLoadBalancing;
528
529impl fmt::Display for ParseErrorLoadBalancing {
530    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
531        write!(f, "Cannot find the load balancing policy asked")
532    }
533}
534
535impl error::Error for ParseErrorLoadBalancing {
536    fn description(&self) -> &str {
537        "Cannot find the load balancing policy asked"
538    }
539
540    fn cause(&self) -> Option<&dyn error::Error> {
541        None
542    }
543}
544
545impl FromStr for LoadBalancingAlgorithms {
546    type Err = ParseErrorLoadBalancing;
547
548    fn from_str(s: &str) -> Result<Self, Self::Err> {
549        match s.to_lowercase().as_str() {
550            "round_robin" => Ok(LoadBalancingAlgorithms::RoundRobin),
551            "random" => Ok(LoadBalancingAlgorithms::Random),
552            "power_of_two" => Ok(LoadBalancingAlgorithms::PowerOfTwo),
553            "least_loaded" => Ok(LoadBalancingAlgorithms::LeastLoaded),
554            "hrw" => Ok(LoadBalancingAlgorithms::Hrw),
555            "maglev" => Ok(LoadBalancingAlgorithms::Maglev),
556            _ => Err(ParseErrorLoadBalancing {}),
557        }
558    }
559}
560
561impl SocketAddress {
562    pub fn new_v4(a: u8, b: u8, c: u8, d: u8, port: u16) -> Self {
563        SocketAddr::new(IpAddr::V4(Ipv4Addr::new(a, b, c, d)), port).into()
564    }
565}
566
567impl From<SocketAddr> for SocketAddress {
568    fn from(socket_addr: SocketAddr) -> SocketAddress {
569        let ip_inner = match socket_addr {
570            SocketAddr::V4(ip_v4_addr) => ip_address::Inner::V4(u32::from(*ip_v4_addr.ip())),
571            SocketAddr::V6(ip_v6_addr) => {
572                ip_address::Inner::V6(Uint128::from(u128::from(*ip_v6_addr.ip())))
573            }
574        };
575
576        let encoded = SocketAddress {
577            port: socket_addr.port() as u32,
578            ip: IpAddress {
579                inner: Some(ip_inner),
580            },
581        };
582
583        // POST: the port widens losslessly (u16 → u32) and the proto address
584        // family matches the source family — a V4 SocketAddr must never encode
585        // as a V6 inner and vice versa, or the reverse `From` would synthesize
586        // the wrong address.
587        debug_assert_eq!(
588            encoded.port,
589            socket_addr.port() as u32,
590            "port must round-trip losslessly into the proto"
591        );
592        debug_assert_eq!(
593            matches!(encoded.ip.inner, Some(ip_address::Inner::V4(_))),
594            socket_addr.is_ipv4(),
595            "proto IP family must match the source SocketAddr family"
596        );
597        encoded
598    }
599}
600
601impl From<SocketAddress> for SocketAddr {
602    fn from(socket_address: SocketAddress) -> Self {
603        // PRE: a wire-sourced proto port may exceed u16::MAX (16-bit on the
604        // wire is carried as a 32-bit field). This is peer/config input, so we
605        // narrow rather than panic; the debug_assert only guards our *own*
606        // encoders, which never emit an out-of-range port.
607        debug_assert!(
608            socket_address.port <= u16::MAX as u32,
609            "self-encoded proto port must fit in a u16"
610        );
611        let had_inner = socket_address.ip.inner.is_some();
612        let port = socket_address.port as u16;
613
614        let ip = match socket_address.ip.inner {
615            Some(inner) => match inner {
616                ip_address::Inner::V4(v4_value) => IpAddr::V4(Ipv4Addr::from(v4_value)),
617                ip_address::Inner::V6(v6_value) => IpAddr::V6(Ipv6Addr::from(u128::from(v6_value))),
618            },
619            None => IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), // should never happen
620        };
621
622        let decoded = SocketAddr::new(ip, port);
623        // POST: a self-encoded proto address always carries an inner IP, so the
624        // unspecified-V4 fallback is only ever reached on malformed peer input.
625        debug_assert!(
626            had_inner || decoded.ip() == IpAddr::V4(Ipv4Addr::UNSPECIFIED),
627            "missing inner IP must decode to the unspecified-V4 sentinel"
628        );
629        decoded
630    }
631}
632
633impl From<Uint128> for u128 {
634    fn from(value: Uint128) -> Self {
635        let combined = value.low as u128 | ((value.high as u128) << 64);
636        // POST: the two 64-bit halves occupy disjoint bit ranges, so the low
637        // half is recoverable as the bottom 64 bits and the high half as the
638        // top 64 bits — the pack is bijective.
639        debug_assert_eq!(
640            combined as u64, value.low,
641            "low half must be the bottom 64 bits"
642        );
643        debug_assert_eq!(
644            (combined >> 64) as u64,
645            value.high,
646            "high half must be the top 64 bits"
647        );
648        combined
649    }
650}
651
652impl From<u128> for Uint128 {
653    fn from(value: u128) -> Self {
654        let low = value as u64;
655        let high = (value >> 64) as u64;
656        let packed = Uint128 { low, high };
657        // POST: splitting then recombining reproduces the original u128 — the
658        // split-into-halves and join-from-halves operations are mutual
659        // inverses (no bit is lost or duplicated).
660        debug_assert_eq!(
661            u128::from(Uint128 {
662                low: packed.low,
663                high: packed.high
664            }),
665            value,
666            "u128 → Uint128 → u128 must round-trip"
667        );
668        packed
669    }
670}
671
672impl From<i128> for Uint128 {
673    fn from(value: i128) -> Self {
674        Uint128::from(value as u128)
675    }
676}
677
678impl From<Ulid> for Uint128 {
679    fn from(value: Ulid) -> Self {
680        let (low, high) = value.into();
681        let packed = Uint128 { low, high };
682        // POST: the (low, high) tuple is carried verbatim into the proto, so
683        // re-reading it reconstructs the same Ulid — the encoding loses no bits.
684        debug_assert_eq!(
685            Ulid::from((packed.low, packed.high)),
686            value,
687            "Ulid → Uint128 must preserve all 128 bits"
688        );
689        packed
690    }
691}
692
693impl From<Uint128> for Ulid {
694    fn from(value: Uint128) -> Self {
695        let Uint128 { low, high } = value;
696        let ulid = Ulid::from((low, high));
697        // POST: the decode is the exact inverse of the encode above — the same
698        // halves go back out, so Uint128 → Ulid → Uint128 round-trips.
699        debug_assert_eq!(
700            Uint128::from(ulid),
701            value,
702            "Uint128 → Ulid must preserve all 128 bits"
703        );
704        ulid
705    }
706}
707
708#[cfg(test)]
709mod tests {
710    use prost::Message;
711
712    use super::*;
713    use crate::proto::command::{HeaderMatchKind, HeaderPredicate};
714
715    fn https_listener(client_auth: Option<i32>) -> HttpsListenerConfig {
716        HttpsListenerConfig {
717            address: SocketAddress::new_v4(127, 0, 0, 1, 8443),
718            client_auth,
719            client_ca_certificates: vec!["CLIENT CA PEM".to_owned()],
720            ..Default::default()
721        }
722    }
723
724    const NONE: i32 = ClientAuthMode::ClientAuthNone as i32;
725    const OPTIONAL: i32 = ClientAuthMode::ClientAuthOptional as i32;
726    const REQUIRED: i32 = ClientAuthMode::ClientAuthRequired as i32;
727
728    #[test]
729    fn add_https_listener_picks_the_verb_from_the_client_auth_mode() {
730        for client_auth in [None, Some(NONE)] {
731            assert!(
732                matches!(
733                    RequestType::add_https_listener(https_listener(client_auth)),
734                    RequestType::AddHttpsListener(_)
735                ),
736                "a listener without client auth ({client_auth:?}) keeps the verb every worker decodes"
737            );
738        }
739        // 999 names no mode: it must not travel on the verb an old worker
740        // would decode and silently strip, the worker rejects it at build.
741        for client_auth in [Some(OPTIONAL), Some(REQUIRED), Some(999)] {
742            assert!(
743                matches!(
744                    RequestType::add_https_listener(https_listener(client_auth)),
745                    RequestType::AddHttpsListenerWithClientAuth(_)
746                ),
747                "a listener asking for a client certificate ({client_auth:?}) needs the mTLS verb"
748            );
749        }
750    }
751
752    /// A worker that predates `RedirectPolicy::SilentClose` resolves the
753    /// unknown enum value to `Forward` and would forward the traffic, so a
754    /// silent-close frontend must leave on a verb such a worker cannot decode.
755    #[test]
756    fn silent_close_frontends_travel_on_their_own_verb() {
757        let front = |redirect: Option<RedirectPolicy>| RequestHttpFrontend {
758            hostname: "example.com".to_owned(),
759            redirect: redirect.map(|policy| policy as i32),
760            ..Default::default()
761        };
762        let silent = front(Some(RedirectPolicy::SilentClose));
763        assert_eq!(
764            RequestType::AddHttpFrontend(silent.clone()).into_canonical(),
765            RequestType::AddHttpFrontendWithSilentClose(silent.clone()),
766            "silent close sent on the historical verb must be moved to its own verb"
767        );
768        assert_eq!(
769            RequestType::AddHttpsFrontend(silent.clone()).into_canonical(),
770            RequestType::AddHttpsFrontendWithSilentClose(silent.clone()),
771        );
772        for policy in [
773            None,
774            Some(RedirectPolicy::Forward),
775            Some(RedirectPolicy::Unauthorized),
776        ] {
777            let plain = front(policy);
778            assert_eq!(
779                RequestType::AddHttpFrontendWithSilentClose(plain.clone()).into_canonical(),
780                RequestType::AddHttpFrontend(plain.clone()),
781                "a frontend that is not silent close keeps the verb every worker decodes"
782            );
783            assert_eq!(
784                RequestType::AddHttpsFrontendWithSilentClose(plain.clone()).into_canonical(),
785                RequestType::AddHttpsFrontend(plain),
786            );
787        }
788    }
789
790    /// The silent-close verbs sit on tags 70 and 71. Tags 66-69 belong to
791    /// the header-predicate verbs and 72 to `AddUnixBackend`, and a
792    /// worker that predates these tags sees an unknown `oneof` member, so the
793    /// request carries no route there instead of a forwarding one.
794    #[test]
795    fn silent_close_verbs_use_their_allocated_wire_tags() {
796        // A 2.3.1 worker drops the silent-close verbs as unknown members and
797        // keeps decoding the historical verbs of a non-silent frontend.
798        let silent = RequestHttpFrontend {
799            hostname: "example.com".to_owned(),
800            redirect: Some(RedirectPolicy::SilentClose as i32),
801            ..Default::default()
802        };
803        assert_eq!(
804            decode_on_a_legacy_worker(RequestType::add_http_frontend(silent.clone())),
805            None
806        );
807        assert_eq!(
808            decode_on_a_legacy_worker(RequestType::add_https_frontend(silent.clone())),
809            None
810        );
811        let plain = RequestHttpFrontend {
812            redirect: Some(RedirectPolicy::Forward as i32),
813            ..silent
814        };
815        assert_eq!(
816            decode_on_a_legacy_worker(RequestType::add_http_frontend(plain.clone())),
817            Some(LegacyRequestType::AddHttpFrontend(plain.clone()))
818        );
819        assert_eq!(
820            decode_on_a_legacy_worker(RequestType::add_https_frontend(plain.clone())),
821            Some(LegacyRequestType::AddHttpsFrontend(plain))
822        );
823
824        let front = RequestHttpFrontend {
825            hostname: "example.com".to_owned(),
826            redirect: Some(RedirectPolicy::SilentClose as i32),
827            ..Default::default()
828        };
829        let encode = |request_type| {
830            Request {
831                request_type: Some(request_type),
832            }
833            .encode_to_vec()
834        };
835        // key = (tag << 3) | length-delimited(2), as a varint
836        assert_eq!(
837            &encode(RequestType::AddHttpFrontendWithSilentClose(front.clone()))[..2],
838            &[0xB2, 0x04],
839            "tag 70"
840        );
841        assert_eq!(
842            &encode(RequestType::AddHttpsFrontendWithSilentClose(front.clone()))[..2],
843            &[0xBA, 0x04],
844            "tag 71"
845        );
846        // the legacy verbs keep their historical tags
847        let legacy = encode(RequestType::AddHttpFrontend(front));
848        assert_ne!(&legacy[..2], &[0xB2, 0x04]);
849    }
850
851    #[test]
852    fn into_canonical_moves_https_listeners_to_their_verb_both_ways() {
853        let required = https_listener(Some(REQUIRED));
854        assert_eq!(
855            RequestType::AddHttpsListener(required.clone()).into_canonical(),
856            RequestType::AddHttpsListenerWithClientAuth(required.clone()),
857            "client auth sent on the historical verb must be moved to the mTLS verb"
858        );
859        assert_eq!(
860            RequestType::AddHttpsListenerWithClientAuth(required.clone()).into_canonical(),
861            RequestType::AddHttpsListenerWithClientAuth(required),
862        );
863
864        let none = https_listener(None);
865        assert_eq!(
866            RequestType::AddHttpsListenerWithClientAuth(none.clone()).into_canonical(),
867            RequestType::AddHttpsListener(none.clone()),
868            "a listener without client auth goes back to the verb every worker decodes"
869        );
870        assert_eq!(
871            Request::from(RequestType::AddHttpsListener(none.clone())).into_canonical(),
872            Request::from(RequestType::AddHttpsListener(none)),
873        );
874
875        let other = RequestType::SoftStop(Default::default());
876        assert_eq!(other.clone().into_canonical(), other);
877        assert_eq!(
878            Request { request_type: None }.into_canonical(),
879            Request { request_type: None }
880        );
881    }
882
883    /// The `WorkerRequest` schema of a worker that predates mutual TLS, cut
884    /// down to what matters here: the envelope, and the one verb such a
885    /// worker had for adding an HTTPS listener. prost skips every tag a
886    /// schema does not declare, exactly as it does for the verbs elided here.
887    #[derive(Clone, PartialEq, Message)]
888    struct LegacyWorkerRequest {
889        #[prost(string, tag = "1")]
890        id: String,
891        #[prost(message, optional, tag = "2")]
892        content: Option<LegacyRequest>,
893    }
894
895    #[derive(Clone, PartialEq, Message)]
896    struct LegacyRequest {
897        #[prost(oneof = "LegacyRequestType", tags = "15, 17, 27, 48")]
898        request_type: Option<LegacyRequestType>,
899    }
900
901    #[derive(Clone, PartialEq, prost::Oneof)]
902    enum LegacyRequestType {
903        /// `add_http_frontend`, understood by every worker.
904        #[prost(message, tag = "15")]
905        AddHttpFrontend(RequestHttpFrontend),
906        /// `add_https_frontend`, understood by every worker.
907        #[prost(message, tag = "17")]
908        AddHttpsFrontend(RequestHttpFrontend),
909        /// `add_https_listener`, holding an `HttpsListenerConfig` that has no
910        /// client authentication field.
911        #[prost(message, tag = "27")]
912        AddHttpsListener(LegacyHttpsListenerConfig),
913        /// `update_https_listener`, holding an `UpdateHttpsListenerConfig`
914        /// that has no client authentication policy field.
915        #[prost(message, tag = "48")]
916        UpdateHttpsListener(LegacyHttpsListenerConfig),
917    }
918
919    #[derive(Clone, PartialEq, Message)]
920    struct LegacyHttpsListenerConfig {
921        #[prost(message, optional, tag = "1")]
922        address: Option<SocketAddress>,
923    }
924
925    fn decode_on_a_legacy_worker(request: RequestType) -> Option<LegacyRequestType> {
926        let bytes = WorkerRequest::new("ID-1".to_owned(), request.into()).encode_to_vec();
927        let legacy = LegacyWorkerRequest::decode(bytes.as_slice())
928            .expect("a legacy worker decodes the envelope of every request");
929        assert_eq!(legacy.id, "ID-1");
930        legacy
931            .content
932            .expect("the envelope always carries a request")
933            .request_type
934    }
935
936    /// A main process that knows mutual TLS talking to a worker that does
937    /// not, the state between `UpgradeMain` and the replacement of the last
938    /// old worker.
939    #[test]
940    fn a_worker_that_predates_mtls_cannot_decode_a_listener_that_requires_it() {
941        let required = https_listener(Some(REQUIRED));
942
943        // The failure mode the dedicated verb exists for: on the historical
944        // verb, the old worker sees a plain HTTPS listener, client auth gone.
945        assert!(
946            matches!(
947                decode_on_a_legacy_worker(RequestType::AddHttpsListener(required.clone())),
948                Some(LegacyRequestType::AddHttpsListener(_))
949            ),
950            "precondition: the historical verb decodes on an old worker without its client auth"
951        );
952
953        // What the main process actually sends: nothing the old worker can
954        // build a listener from, so it fails the request instead.
955        assert_eq!(
956            decode_on_a_legacy_worker(
957                RequestType::AddHttpsListener(required.clone()).into_canonical()
958            ),
959            None,
960            "a listener that requires a client certificate must be undecodable by an old worker"
961        );
962        assert_eq!(
963            decode_on_a_legacy_worker(RequestType::add_https_listener(required)),
964            None
965        );
966
967        // Positive space: a listener without client auth stays readable, so
968        // a mixed fleet keeps serving every listener it can serve safely.
969        assert_eq!(
970            decode_on_a_legacy_worker(RequestType::add_https_listener(https_listener(None))),
971            Some(LegacyRequestType::AddHttpsListener(
972                LegacyHttpsListenerConfig {
973                    address: Some(SocketAddress::new_v4(127, 0, 0, 1, 8443)),
974                }
975            ))
976        );
977    }
978
979    fn policy_patch(policy: Option<ClientAuthPolicy>) -> UpdateHttpsListenerConfig {
980        UpdateHttpsListenerConfig {
981            address: SocketAddress::new_v4(127, 0, 0, 1, 8443),
982            client_auth_policy: policy,
983            ..Default::default()
984        }
985    }
986
987    #[test]
988    fn update_https_listener_picks_the_verb_from_the_policy_field() {
989        assert!(matches!(
990            RequestType::update_https_listener(policy_patch(None)),
991            RequestType::UpdateHttpsListener(_)
992        ));
993        // Any policy, even one turning client auth off: a worker that ignored
994        // it would keep enforcing, or keep trusting, what the patch replaces.
995        for client_auth in [None, Some(NONE), Some(REQUIRED)] {
996            let policy = ClientAuthPolicy {
997                client_auth,
998                ..Default::default()
999            };
1000            assert!(
1001                matches!(
1002                    RequestType::update_https_listener(policy_patch(Some(policy))),
1003                    RequestType::UpdateHttpsListenerWithClientAuth(_)
1004                ),
1005                "a patch carrying a policy ({client_auth:?}) needs the mTLS verb"
1006            );
1007        }
1008        let with_policy = policy_patch(Some(ClientAuthPolicy::default()));
1009        assert_eq!(
1010            RequestType::UpdateHttpsListener(with_policy.clone()).into_canonical(),
1011            RequestType::UpdateHttpsListenerWithClientAuth(with_policy)
1012        );
1013        assert_eq!(
1014            RequestType::UpdateHttpsListenerWithClientAuth(policy_patch(None)).into_canonical(),
1015            RequestType::UpdateHttpsListener(policy_patch(None))
1016        );
1017    }
1018
1019    #[test]
1020    fn a_worker_that_predates_mtls_cannot_decode_a_client_auth_policy_patch() {
1021        let refresh = policy_patch(Some(https_listener(Some(REQUIRED)).client_auth_policy()));
1022        assert!(
1023            matches!(
1024                decode_on_a_legacy_worker(RequestType::UpdateHttpsListener(refresh.clone())),
1025                Some(LegacyRequestType::UpdateHttpsListener(_))
1026            ),
1027            "precondition: the historical verb decodes on an old worker without its policy"
1028        );
1029        assert_eq!(
1030            decode_on_a_legacy_worker(RequestType::update_https_listener(refresh)),
1031            None,
1032            "a patch replacing the client auth policy must be undecodable by an old worker"
1033        );
1034        assert!(matches!(
1035            decode_on_a_legacy_worker(RequestType::update_https_listener(policy_patch(None))),
1036            Some(LegacyRequestType::UpdateHttpsListener(_))
1037        ));
1038    }
1039
1040    #[test]
1041    fn applying_a_client_auth_policy_replaces_all_of_it() {
1042        let mut listener = https_listener(Some(REQUIRED));
1043        listener.client_ca_crls = vec!["OLD CRL".to_owned()];
1044        let policy = ClientAuthPolicy {
1045            client_auth: Some(OPTIONAL),
1046            client_ca_certificates: vec!["NEW CA".to_owned()],
1047            client_ca_crls: vec![],
1048        };
1049        listener.apply_client_auth_policy(&policy);
1050        assert_eq!(listener.client_auth_policy(), policy);
1051        assert!(
1052            listener.client_ca_crls.is_empty(),
1053            "a CRL the policy leaves out is dropped, not kept"
1054        );
1055    }
1056
1057    /// `WorkerRequest` of a worker that predates header routing: the four
1058    /// historical frontend verbs, holding a `RequestHttpFrontend` that has no
1059    /// `header_predicates` field.
1060    #[derive(Clone, PartialEq, Message)]
1061    struct LegacyFrontendWorkerRequest {
1062        #[prost(string, tag = "1")]
1063        id: String,
1064        #[prost(message, optional, tag = "2")]
1065        content: Option<LegacyFrontendRequest>,
1066    }
1067
1068    #[derive(Clone, PartialEq, Message)]
1069    struct LegacyFrontendRequest {
1070        #[prost(oneof = "LegacyFrontendRequestType", tags = "15, 16, 17, 18")]
1071        request_type: Option<LegacyFrontendRequestType>,
1072    }
1073
1074    #[derive(Clone, PartialEq, prost::Oneof)]
1075    enum LegacyFrontendRequestType {
1076        #[prost(message, tag = "15")]
1077        AddHttp(LegacyHttpFrontend),
1078        #[prost(message, tag = "16")]
1079        RemoveHttp(LegacyHttpFrontend),
1080        #[prost(message, tag = "17")]
1081        AddHttps(LegacyHttpFrontend),
1082        #[prost(message, tag = "18")]
1083        RemoveHttps(LegacyHttpFrontend),
1084    }
1085
1086    #[derive(Clone, PartialEq, Message)]
1087    struct LegacyHttpFrontend {
1088        #[prost(string, tag = "3")]
1089        hostname: String,
1090    }
1091
1092    fn decode_frontend_on_a_legacy_worker(
1093        request: RequestType,
1094    ) -> Option<LegacyFrontendRequestType> {
1095        let bytes = WorkerRequest::new("ID-1".to_owned(), request.into()).encode_to_vec();
1096        LegacyFrontendWorkerRequest::decode(bytes.as_slice())
1097            .expect("a legacy worker decodes the envelope of every request")
1098            .content
1099            .expect("the envelope always carries a request")
1100            .request_type
1101    }
1102
1103    fn frontend(with_predicate: bool) -> RequestHttpFrontend {
1104        RequestHttpFrontend {
1105            cluster_id: Some("canary".to_owned()),
1106            address: SocketAddress::new_v4(127, 0, 0, 1, 8080),
1107            hostname: "example.com".to_owned(),
1108            header_predicates: if with_predicate {
1109                vec![HeaderPredicate {
1110                    name: "X-Canary".to_owned(),
1111                    kind: HeaderMatchKind::Present as i32,
1112                    value: None,
1113                }]
1114            } else {
1115                Vec::new()
1116            },
1117            ..Default::default()
1118        }
1119    }
1120
1121    #[test]
1122    fn frontend_verbs_follow_the_header_predicates() {
1123        let guarded = frontend(true);
1124        let plain = frontend(false);
1125        assert!(matches!(
1126            RequestType::add_http_frontend(guarded.clone()),
1127            RequestType::AddHttpFrontendWithHeaderPredicates(_)
1128        ));
1129        assert!(matches!(
1130            RequestType::remove_http_frontend(guarded.clone()),
1131            RequestType::RemoveHttpFrontendWithHeaderPredicates(_)
1132        ));
1133        assert!(matches!(
1134            RequestType::add_https_frontend(guarded.clone()),
1135            RequestType::AddHttpsFrontendWithHeaderPredicates(_)
1136        ));
1137        assert!(matches!(
1138            RequestType::remove_https_frontend(guarded.clone()),
1139            RequestType::RemoveHttpsFrontendWithHeaderPredicates(_)
1140        ));
1141        // routes without predicates keep the historical verbs
1142        assert_eq!(
1143            RequestType::add_http_frontend(plain.clone()),
1144            RequestType::AddHttpFrontend(plain.clone())
1145        );
1146        assert_eq!(
1147            RequestType::remove_https_frontend(plain.clone()),
1148            RequestType::RemoveHttpsFrontend(plain.clone())
1149        );
1150        // whatever verb the client used, the fan-out sends the canonical one
1151        assert_eq!(
1152            RequestType::AddHttpFrontend(guarded.clone()).into_canonical(),
1153            RequestType::AddHttpFrontendWithHeaderPredicates(guarded.clone())
1154        );
1155        assert_eq!(
1156            RequestType::RemoveHttpsFrontendWithHeaderPredicates(plain.clone()).into_canonical(),
1157            RequestType::RemoveHttpsFrontend(plain)
1158        );
1159    }
1160
1161    #[test]
1162    fn a_worker_that_predates_header_routing_cannot_decode_a_guarded_frontend() {
1163        let guarded = frontend(true);
1164        // The failure mode the dedicated verbs exist for: on a historical
1165        // verb the old worker installs the route and drops its guard.
1166        assert!(
1167            matches!(
1168                decode_frontend_on_a_legacy_worker(RequestType::AddHttpFrontend(guarded.clone())),
1169                Some(LegacyFrontendRequestType::AddHttp(_))
1170            ),
1171            "precondition: the historical verb decodes on an old worker without its guard"
1172        );
1173        for request in [
1174            RequestType::add_http_frontend(guarded.clone()),
1175            RequestType::remove_http_frontend(guarded.clone()),
1176            RequestType::add_https_frontend(guarded.clone()),
1177            RequestType::remove_https_frontend(guarded.clone()),
1178            RequestType::AddHttpsFrontend(guarded.clone()).into_canonical(),
1179            RequestType::RemoveHttpFrontend(guarded.clone()).into_canonical(),
1180        ] {
1181            assert_eq!(
1182                decode_frontend_on_a_legacy_worker(request),
1183                None,
1184                "a guarded frontend must be undecodable by an old worker"
1185            );
1186        }
1187        // Positive space: unguarded frontends stay readable in a mixed fleet.
1188        assert_eq!(
1189            decode_frontend_on_a_legacy_worker(RequestType::add_http_frontend(frontend(false))),
1190            Some(LegacyFrontendRequestType::AddHttp(LegacyHttpFrontend {
1191                hostname: "example.com".to_owned()
1192            }))
1193        );
1194    }
1195
1196    /// The tags are wire contract: 65 belongs to `close_sessions`, 70-72 to
1197    /// other pending verbs, so a drift here collides with them.
1198    #[test]
1199    fn header_predicate_verbs_use_the_allocated_wire_tags() {
1200        let guarded = frontend(true);
1201        for (request, tag) in [
1202            (RequestType::add_http_frontend(guarded.clone()), 66u32),
1203            (RequestType::remove_http_frontend(guarded.clone()), 67),
1204            (RequestType::add_https_frontend(guarded.clone()), 68),
1205            (RequestType::remove_https_frontend(guarded.clone()), 69),
1206        ] {
1207            let bytes = Request::from(request).encode_to_vec();
1208            // first byte is the key of the oneof field: (tag << 3) | length-delimited
1209            let (key, _) = prost::encoding::decode_varint(&mut bytes.as_slice())
1210                .map(|k| (k, ()))
1211                .expect("a request starts with a field key");
1212            assert_eq!(key >> 3, u64::from(tag));
1213            assert_eq!(key & 7, 2);
1214        }
1215    }
1216}