Skip to main content

pingora_core/upstreams/
peer.rs

1// Copyright 2026 Cloudflare, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Defines where to connect to and how to connect to a remote server
16
17use crate::connectors::{l4::BindTo, L4Connect};
18use crate::protocols::l4::socket::SocketAddr;
19use crate::protocols::tls::CaType;
20#[cfg(feature = "openssl_derived")]
21use crate::protocols::tls::HandshakeCompleteHook;
22#[cfg(feature = "s2n")]
23use crate::protocols::tls::PskType;
24#[cfg(unix)]
25use crate::protocols::ConnFdReusable;
26use crate::protocols::TcpKeepalive;
27use crate::utils::tls::{get_organization_unit, CertKey};
28use ahash::AHasher;
29use derivative::Derivative;
30use pingora_error::{
31    ErrorType::{InternalError, SocketError},
32    OrErr, Result,
33};
34#[cfg(feature = "s2n")]
35use pingora_s2n::S2NPolicy;
36use std::borrow::Cow;
37use std::collections::BTreeMap;
38use std::fmt::{Display, Formatter, Result as FmtResult};
39use std::hash::{Hash, Hasher};
40use std::net::{IpAddr, SocketAddr as InetSocketAddr, ToSocketAddrs as ToInetSocketAddrs};
41#[cfg(unix)]
42use std::os::unix::{net::SocketAddr as UnixSocketAddr, prelude::AsRawFd};
43#[cfg(windows)]
44use std::os::windows::io::AsRawSocket;
45use std::path::{Path, PathBuf};
46use std::sync::Arc;
47use std::time::Duration;
48use tokio::net::TcpSocket;
49
50pub use crate::protocols::tls::ALPN;
51
52/// A hook function that may generate user data for [`crate::protocols::raw_connect::ProxyDigest`].
53///
54/// Takes the request and response headers from the proxy connection establishment, and may produce
55/// arbitrary data to be stored in ProxyDigest's user_data field.
56///
57/// This can be useful when, for example, you want to store some parameter(s) from the request or
58/// response headers from when the proxy connection was first established.
59pub type ProxyDigestUserDataHook = Arc<
60    dyn Fn(
61            &http::request::Parts,         // request headers
62            &pingora_http::ResponseHeader, // response headers
63        ) -> Option<Box<dyn std::any::Any + Send + Sync>>
64        + Send
65        + Sync
66        + 'static,
67>;
68
69/// The interface to trace the connection
70pub trait Tracing: Send + Sync + std::fmt::Debug {
71    /// This method is called when successfully connected to a remote server
72    fn on_connected(&self);
73    /// This method is called when the connection is disconnected.
74    fn on_disconnected(&self);
75    /// A way to clone itself
76    fn boxed_clone(&self) -> Box<dyn Tracing>;
77}
78
79/// An object-safe version of Tracing object that can use Clone
80#[derive(Debug)]
81pub struct Tracer(pub Box<dyn Tracing>);
82
83impl Clone for Tracer {
84    fn clone(&self) -> Self {
85        Tracer(self.0.boxed_clone())
86    }
87}
88
89/// [`Peer`] defines the interface to communicate with the [`crate::connectors`] regarding where to
90/// connect to and how to connect to it.
91pub trait Peer: Display + Clone {
92    /// The remote address to connect to
93    fn address(&self) -> &SocketAddr;
94    /// If TLS should be used;
95    fn tls(&self) -> bool;
96    /// The SNI to send, if TLS is used
97    fn sni(&self) -> &str;
98    /// To decide whether a [`Peer`] can use the connection established by another [`Peer`].
99    ///
100    /// The connections to two peers are considered reusable to each other if their reuse hashes are
101    /// the same
102    fn reuse_hash(&self) -> u64;
103    /// Get the proxy setting to connect to the remote server
104    fn get_proxy(&self) -> Option<&Proxy> {
105        None
106    }
107    /// Get the additional options to connect to the peer.
108    ///
109    /// See [`PeerOptions`] for more details
110    fn get_peer_options(&self) -> Option<&PeerOptions> {
111        None
112    }
113    /// Get the additional options for modification.
114    fn get_mut_peer_options(&mut self) -> Option<&mut PeerOptions> {
115        None
116    }
117    /// Whether the TLS handshake should validate the cert of the server.
118    fn verify_cert(&self) -> bool {
119        match self.get_peer_options() {
120            Some(opt) => opt.verify_cert,
121            None => false,
122        }
123    }
124    /// Whether the TLS handshake should verify that the server cert matches the SNI.
125    fn verify_hostname(&self) -> bool {
126        match self.get_peer_options() {
127            Some(opt) => opt.verify_hostname,
128            None => false,
129        }
130    }
131    /// Whether the system trust store should be loaded and used when verifying certificates
132    #[cfg(feature = "s2n")]
133    fn use_system_certs(&self) -> bool {
134        match self.get_peer_options() {
135            Some(opt) => opt.use_system_certs,
136            None => false,
137        }
138    }
139    /// The alternative common name to use to verify the server cert.
140    ///
141    /// If the server cert doesn't match the SNI, this name will be used to
142    /// verify the cert.
143    fn alternative_cn(&self) -> Option<&String> {
144        match self.get_peer_options() {
145            Some(opt) => opt.alternative_cn.as_ref(),
146            None => None,
147        }
148    }
149    /// Information about the local source address this connection should be bound to.
150    fn bind_to(&self) -> Option<&BindTo> {
151        match self.get_peer_options() {
152            Some(opt) => opt.bind_to.as_ref(),
153            None => None,
154        }
155    }
156    /// How long connect() call should be wait before it returns a timeout error.
157    fn connection_timeout(&self) -> Option<Duration> {
158        match self.get_peer_options() {
159            Some(opt) => opt.connection_timeout,
160            None => None,
161        }
162    }
163    /// How long the overall connection establishment should take before a timeout error is returned.
164    fn total_connection_timeout(&self) -> Option<Duration> {
165        match self.get_peer_options() {
166            Some(opt) => opt.total_connection_timeout,
167            None => None,
168        }
169    }
170    /// If the connection can be reused, how long the connection should wait to be reused before it
171    /// shuts down.
172    fn idle_timeout(&self) -> Option<Duration> {
173        self.get_peer_options().and_then(|o| o.idle_timeout)
174    }
175
176    /// Get the ALPN preference.
177    fn get_alpn(&self) -> Option<&ALPN> {
178        self.get_peer_options().map(|opt| &opt.alpn)
179    }
180
181    /// Get the CA cert to use to validate the server cert.
182    ///
183    /// If not set, the default CAs will be used.
184    fn get_ca(&self) -> Option<&Arc<CaType>> {
185        match self.get_peer_options() {
186            Some(opt) => opt.ca.as_ref(),
187            None => None,
188        }
189    }
190
191    /// Get the client cert and key for mutual TLS if any
192    fn get_client_cert_key(&self) -> Option<&Arc<CertKey>> {
193        None
194    }
195
196    /// Get the PSK (pre-shared key) to use to validate the connection
197    ///
198    /// If not set, PSK validation will not be used
199    #[cfg(feature = "s2n")]
200    fn get_psk(&self) -> Option<&Arc<PskType>> {
201        match self.get_peer_options() {
202            Some(opt) => opt.psk.as_ref(),
203            None => None,
204        }
205    }
206
207    /// Get the Security Policy to use for this connection (S2N only)
208    ///
209    /// If not set, the default policy "default_tls13" will be used
210    /// https://aws.github.io/s2n-tls/usage-guide/ch06-security-policies.html
211    #[cfg(feature = "s2n")]
212    fn get_s2n_security_policy(&self) -> Option<&S2NPolicy> {
213        match self.get_peer_options() {
214            Some(opt) => opt.s2n_security_policy.as_ref(),
215            None => None,
216        }
217    }
218
219    /// S2N-TLS will delay a response up to the max blinding delay (default 30)
220    /// seconds whenever an error triggered by a peer occurs to mitigate against
221    /// timing side channels.
222    #[cfg(feature = "s2n")]
223    fn get_max_blinding_delay(&self) -> Option<u32> {
224        match self.get_peer_options() {
225            Some(opt) => opt.max_blinding_delay,
226            None => None,
227        }
228    }
229
230    /// The TCP keepalive setting that should be applied to this connection
231    fn tcp_keepalive(&self) -> Option<&TcpKeepalive> {
232        self.get_peer_options()
233            .and_then(|o| o.tcp_keepalive.as_ref())
234    }
235
236    /// The interval H2 pings to send to the server if any
237    fn h2_ping_interval(&self) -> Option<Duration> {
238        self.get_peer_options().and_then(|o| o.h2_ping_interval)
239    }
240
241    /// The size of the TCP receive buffer should be limited to. See SO_RCVBUF for more details.
242    fn tcp_recv_buf(&self) -> Option<usize> {
243        self.get_peer_options().and_then(|o| o.tcp_recv_buf)
244    }
245
246    /// The DSCP value that should be applied to the send side of this connection.
247    /// See the [RFC](https://datatracker.ietf.org/doc/html/rfc2474) for more details.
248    fn dscp(&self) -> Option<u8> {
249        self.get_peer_options().and_then(|o| o.dscp)
250    }
251
252    /// Whether to enable TCP fast open.
253    fn tcp_fast_open(&self) -> bool {
254        self.get_peer_options()
255            .map(|o| o.tcp_fast_open)
256            .unwrap_or_default()
257    }
258
259    #[cfg(unix)]
260    fn matches_fd<V: AsRawFd>(&self, fd: V) -> bool {
261        self.address().check_fd_match(fd)
262    }
263
264    #[cfg(windows)]
265    fn matches_sock<V: AsRawSocket>(&self, sock: V) -> bool {
266        use crate::protocols::ConnSockReusable;
267        self.address().check_sock_match(sock)
268    }
269
270    fn get_tracer(&self) -> Option<Tracer> {
271        None
272    }
273
274    /// Returns a hook that should be run before an upstream TCP connection is connected.
275    ///
276    /// This hook can be used to set additional socket options.
277    fn upstream_tcp_sock_tweak_hook(
278        &self,
279    ) -> Option<&Arc<dyn Fn(&TcpSocket) -> Result<()> + Send + Sync + 'static>> {
280        self.get_peer_options()?
281            .upstream_tcp_sock_tweak_hook
282            .as_ref()
283    }
284
285    /// Returns a [`ProxyDigestUserDataHook`] that may generate user data for
286    /// [`crate::protocols::raw_connect::ProxyDigest`] when establishing a new proxy connection.
287    fn proxy_digest_user_data_hook(&self) -> Option<&ProxyDigestUserDataHook> {
288        self.get_peer_options()?
289            .proxy_digest_user_data_hook
290            .as_ref()
291    }
292
293    /// Returns a hook that should be run on TLS handshake completion.
294    ///
295    /// Any value returned from the returned hook (other than `None`) will be stored in the
296    /// `extension` field of `SslDigest`. This allows you to attach custom application-specific
297    /// data to the TLS connection, which will be accessible from the HTTP layer via the
298    /// `SslDigest` attached to the session digest.
299    ///
300    /// Currently only enabled for openssl variants with meaningful `TlsRef`s.
301    #[cfg(feature = "openssl_derived")]
302    fn upstream_tls_handshake_complete_hook(&self) -> Option<&HandshakeCompleteHook> {
303        self.get_peer_options()?
304            .upstream_tls_handshake_complete_hook
305            .as_ref()
306    }
307}
308
309/// A simple TCP or TLS peer without many complicated settings.
310#[derive(Debug, Clone)]
311pub struct BasicPeer {
312    pub _address: SocketAddr,
313    pub sni: String,
314    pub options: PeerOptions,
315}
316
317impl BasicPeer {
318    /// Create a new [`BasicPeer`].
319    pub fn new(address: &str) -> Self {
320        let addr = SocketAddr::Inet(address.parse().unwrap()); // TODO: check error
321        Self::new_from_sockaddr(addr)
322    }
323
324    /// Create a new [`BasicPeer`] with the given path to a Unix domain socket.
325    #[cfg(unix)]
326    pub fn new_uds<P: AsRef<Path>>(path: P) -> Result<Self> {
327        let addr = SocketAddr::Unix(
328            UnixSocketAddr::from_pathname(path.as_ref())
329                .or_err(InternalError, "while creating BasicPeer")?,
330        );
331        Ok(Self::new_from_sockaddr(addr))
332    }
333
334    fn new_from_sockaddr(sockaddr: SocketAddr) -> Self {
335        BasicPeer {
336            _address: sockaddr,
337            sni: "".to_string(), // TODO: add support for SNI
338            options: PeerOptions::new(),
339        }
340    }
341}
342
343impl Display for BasicPeer {
344    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
345        write!(f, "{:?}", self)
346    }
347}
348
349impl Peer for BasicPeer {
350    fn address(&self) -> &SocketAddr {
351        &self._address
352    }
353
354    fn tls(&self) -> bool {
355        !self.sni.is_empty()
356    }
357
358    fn bind_to(&self) -> Option<&BindTo> {
359        None
360    }
361
362    fn sni(&self) -> &str {
363        &self.sni
364    }
365
366    // TODO: change connection pool to accept u64 instead of String
367    fn reuse_hash(&self) -> u64 {
368        let mut hasher = AHasher::default();
369        self._address.hash(&mut hasher);
370        hasher.finish()
371    }
372
373    fn get_peer_options(&self) -> Option<&PeerOptions> {
374        Some(&self.options)
375    }
376}
377
378/// Define whether to connect via http or https
379#[derive(Hash, Clone, Debug, PartialEq)]
380pub enum Scheme {
381    HTTP,
382    HTTPS,
383}
384
385impl Display for Scheme {
386    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
387        match self {
388            Scheme::HTTP => write!(f, "HTTP"),
389            Scheme::HTTPS => write!(f, "HTTPS"),
390        }
391    }
392}
393
394impl Scheme {
395    pub fn from_tls_bool(tls: bool) -> Self {
396        if tls {
397            Self::HTTPS
398        } else {
399            Self::HTTP
400        }
401    }
402}
403
404/// Policy for forwarding request headers to HTTP upstreams.
405///
406/// This policy applies to automatically forwarded downstream request headers. Application code
407/// may deliberately alter the resulting request in its upstream request filter.
408#[derive(Clone, Copy, Debug, Eq, PartialEq)]
409pub struct HttpUpstreamRequestPolicy {
410    /// Strip standard hop-by-hop request fields inherited from the downstream request.
411    ///
412    /// Standard hop-by-hop framing fields are removed. If a non-empty request body is unframed
413    /// after application upstream request filtering, Pingora sends it chunked to an HTTP/1
414    /// upstream.
415    pub strip_hop_by_hop: bool,
416    /// Strip extension fields identified by tokens in the downstream `Connection` header field.
417    ///
418    /// Requests nominating `Host`, forwarding-origin fields, or pseudo-header-shaped fields are
419    /// rejected rather than forwarded with protected metadata removed when this behavior is
420    /// enabled.
421    pub strip_connection_nominated: bool,
422    /// Reject `Connection` nominations that are not a valid HTTP `token` (RFC 9110 ยง5.6.2), such
423    /// as `Connection: "X-Forwarded-For"`, instead of tolerating them as distinct literal fields.
424    ///
425    /// Defaults to `true`. Exact nominations of protected fields and pseudo-headers are rejected
426    /// either way. Only has an effect when
427    /// [`strip_connection_nominated`](Self::strip_connection_nominated) is enabled.
428    pub reject_malformed_connection_nominations: bool,
429    /// Controls forwarding of HTTP/1 protocol upgrade request fields.
430    pub h1_upgrade: H1UpgradePolicy,
431}
432
433impl HttpUpstreamRequestPolicy {
434    /// Use standards-oriented forwarding with normalized WebSocket upgrade support.
435    pub fn standard() -> Self {
436        Self::default()
437    }
438
439    /// Preserve the previous HTTP/1 upstream request-header passthrough behavior.
440    ///
441    /// This mode is RFC-non-compliant and is provided only for legacy compatibility. Use it at
442    /// your own risk: the application's upstream request filter is solely responsible for
443    /// ensuring valid hop-by-hop header handling.
444    pub fn preserve() -> Self {
445        Self {
446            strip_hop_by_hop: false,
447            strip_connection_nominated: false,
448            reject_malformed_connection_nominations: false,
449            h1_upgrade: H1UpgradePolicy::Preserve,
450        }
451    }
452
453    /// Strip hop-by-hop fields and do not forward any HTTP/1 upgrade handshake.
454    pub fn deny_upgrades() -> Self {
455        Self {
456            h1_upgrade: H1UpgradePolicy::Deny,
457            ..Self::default()
458        }
459    }
460}
461
462impl Default for HttpUpstreamRequestPolicy {
463    fn default() -> Self {
464        Self {
465            strip_hop_by_hop: true,
466            strip_connection_nominated: true,
467            reject_malformed_connection_nominations: true,
468            h1_upgrade: H1UpgradePolicy::WebSocketOnly,
469        }
470    }
471}
472
473/// Policy for forwarding HTTP/1 protocol upgrade request fields.
474#[derive(Clone, Copy, Debug, Eq, PartialEq)]
475pub enum H1UpgradePolicy {
476    /// Forward normalized upgrade fields only for a valid WebSocket upgrade request.
477    WebSocketOnly,
478    /// Preserve complete request metadata for HTTP/1 requests containing an upgrade field.
479    ///
480    /// This is RFC-non-compliant and is provided only for legacy compatibility. Use it at your
481    /// own risk: the application's upstream request filter is solely responsible for ensuring
482    /// valid hop-by-hop metadata for upgraded requests.
483    ///
484    /// When this is selected, automatic hop-by-hop normalization is skipped for upgrade requests
485    /// because protocol-specific handshakes can depend on any connection-nominated field.
486    /// Protected nomination validation applies only when
487    /// [`HttpUpstreamRequestPolicy::strip_connection_nominated`] is enabled.
488    Preserve,
489    /// Do not forward HTTP/1 upgrade request fields.
490    Deny,
491}
492
493/// The preferences to connect to a remote server
494///
495/// See [`Peer`] for the meaning of the fields
496#[non_exhaustive]
497#[derive(Clone, Derivative)]
498#[derivative(Debug)]
499pub struct PeerOptions {
500    pub bind_to: Option<BindTo>,
501    pub connection_timeout: Option<Duration>,
502    pub total_connection_timeout: Option<Duration>,
503    pub read_timeout: Option<Duration>,
504    pub idle_timeout: Option<Duration>,
505    pub write_timeout: Option<Duration>,
506    pub verify_cert: bool,
507    pub verify_hostname: bool,
508    #[cfg(feature = "s2n")]
509    pub use_system_certs: bool,
510    /* accept the cert if it's CN matches the SNI or this name */
511    pub alternative_cn: Option<String>,
512    pub alpn: ALPN,
513    pub ca: Option<Arc<CaType>>,
514    pub tcp_keepalive: Option<TcpKeepalive>,
515    pub tcp_recv_buf: Option<usize>,
516    pub dscp: Option<u8>,
517    pub h2_ping_interval: Option<Duration>,
518    #[cfg(feature = "s2n")]
519    pub psk: Option<Arc<PskType>>,
520    #[cfg(feature = "s2n")]
521    pub s2n_security_policy: Option<S2NPolicy>,
522    #[cfg(feature = "s2n")]
523    pub max_blinding_delay: Option<u32>,
524    /// How many concurrent h2 streams are allowed in the same connection.
525    pub max_h2_streams: usize,
526    /// Initial per-stream H2 receive window size in bytes.
527    /// If `None`, the default of 8MB is used.
528    pub h2_stream_window_size: Option<u32>,
529    /// Initial connection-level H2 receive window size in bytes.
530    /// If `None`, the default of 8MB is used.
531    pub h2_connection_window_size: Option<u32>,
532    /// Allow a single invalid Content-Length in HTTP/1 responses (non-RFC compliant).
533    ///
534    /// When enabled, a response carrying a single, otherwise-unparseable
535    /// Content-Length value is treated as a close-delimited response instead of
536    /// being rejected. Conflicting or duplicate Content-Length values (multiple
537    /// header lines, or a comma-separated list with differing values) are an
538    /// unrecoverable framing error and are always rejected, even when this is
539    /// enabled.
540    ///
541    /// **Note:** This field is unstable and may be removed or changed in future versions.
542    /// It exists primarily for compatibility with legacy servers that send malformed headers.
543    pub allow_h1_response_invalid_content_length: bool,
544    /// Controls automatically forwarded request headers sent to HTTP upstreams.
545    pub http_upstream_request_policy: HttpUpstreamRequestPolicy,
546    pub extra_proxy_headers: BTreeMap<String, Vec<u8>>,
547    /// The list of curves the tls connection should advertise
548    /// if `None`, the default curves will be used
549    pub curves: Option<Cow<'static, str>>,
550    /// see ssl_use_second_key_share
551    pub second_keyshare: bool,
552    /// whether to enable TCP fast open
553    pub tcp_fast_open: bool,
554    /// use Arc because Clone is required but not allowed in trait object
555    pub tracer: Option<Tracer>,
556    /// A custom L4 connector to use to establish new L4 connections
557    pub custom_l4: Option<Arc<dyn L4Connect + Send + Sync>>,
558    #[derivative(Debug = "ignore")]
559    pub upstream_tcp_sock_tweak_hook:
560        Option<Arc<dyn Fn(&TcpSocket) -> Result<()> + Send + Sync + 'static>>,
561    #[derivative(Debug = "ignore")]
562    pub proxy_digest_user_data_hook: Option<ProxyDigestUserDataHook>,
563    /// Hook that allows returning an optional `SslDigestExtension`.
564    /// Any returned value will be saved into the `SslDigest`.
565    ///
566    /// Currently only enabled for openssl variants with meaningful `TlsRef`s.
567    #[cfg(feature = "openssl_derived")]
568    #[derivative(Debug = "ignore")]
569    pub upstream_tls_handshake_complete_hook: Option<HandshakeCompleteHook>,
570}
571
572impl PeerOptions {
573    /// Create a new [`PeerOptions`]
574    pub fn new() -> Self {
575        PeerOptions {
576            bind_to: None,
577            connection_timeout: None,
578            total_connection_timeout: None,
579            read_timeout: None,
580            idle_timeout: None,
581            write_timeout: None,
582            verify_cert: true,
583            verify_hostname: true,
584            #[cfg(feature = "s2n")]
585            use_system_certs: true,
586            alternative_cn: None,
587            alpn: ALPN::H1,
588            ca: None,
589            tcp_keepalive: None,
590            tcp_recv_buf: None,
591            dscp: None,
592            h2_ping_interval: None,
593            #[cfg(feature = "s2n")]
594            psk: None,
595            #[cfg(feature = "s2n")]
596            s2n_security_policy: None,
597            #[cfg(feature = "s2n")]
598            max_blinding_delay: None,
599            max_h2_streams: 1,
600            h2_stream_window_size: None,
601            h2_connection_window_size: None,
602            allow_h1_response_invalid_content_length: false,
603            http_upstream_request_policy: HttpUpstreamRequestPolicy::default(),
604            extra_proxy_headers: BTreeMap::new(),
605            curves: None,
606            second_keyshare: true, // default true and noop when not using PQ curves
607            tcp_fast_open: false,
608            tracer: None,
609            custom_l4: None,
610            upstream_tcp_sock_tweak_hook: None,
611            proxy_digest_user_data_hook: None,
612            #[cfg(feature = "openssl_derived")]
613            upstream_tls_handshake_complete_hook: None,
614        }
615    }
616
617    /// Set the ALPN according to the `max` and `min` constrains.
618    pub fn set_http_version(&mut self, max: u8, min: u8) {
619        self.alpn = ALPN::new(max, min);
620    }
621}
622
623impl Display for PeerOptions {
624    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
625        if let Some(b) = self.bind_to.as_ref() {
626            write!(f, "bind_to: {:?},", b)?;
627        }
628        if let Some(t) = self.connection_timeout {
629            write!(f, "conn_timeout: {:?},", t)?;
630        }
631        if let Some(t) = self.total_connection_timeout {
632            write!(f, "total_conn_timeout: {:?},", t)?;
633        }
634        if self.verify_cert {
635            write!(f, "verify_cert: true,")?;
636        }
637        if self.verify_hostname {
638            write!(f, "verify_hostname: true,")?;
639        }
640        #[cfg(feature = "s2n")]
641        if self.use_system_certs {
642            write!(f, "use_system_certs: true,")?;
643        }
644        if let Some(cn) = &self.alternative_cn {
645            write!(f, "alt_cn: {},", cn)?;
646        }
647        write!(f, "alpn: {},", self.alpn)?;
648        if let Some(cas) = &self.ca {
649            for ca in cas.iter() {
650                write!(
651                    f,
652                    "CA: {}, expire: {},",
653                    get_organization_unit(ca).unwrap_or_default(),
654                    ca.not_after()
655                )?;
656            }
657        }
658        #[cfg(feature = "s2n")]
659        if let Some(policy) = &self.s2n_security_policy {
660            write!(f, "s2n_security_policy: {:?}, ", policy)?;
661        }
662        #[cfg(feature = "s2n")]
663        if let Some(psk_config) = &self.psk {
664            for psk in &psk_config.keys {
665                write!(
666                    f,
667                    "psk_identity: {}",
668                    String::from_utf8_lossy(psk.identity.as_slice())
669                )?;
670            }
671        }
672        if let Some(tcp_keepalive) = &self.tcp_keepalive {
673            write!(f, "tcp_keepalive: {},", tcp_keepalive)?;
674        }
675        if let Some(h2_ping_interval) = self.h2_ping_interval {
676            write!(f, "h2_ping_interval: {:?},", h2_ping_interval)?;
677        }
678        Ok(())
679    }
680}
681
682/// A peer representing the remote HTTP server to connect to
683#[derive(Debug, Clone)]
684pub struct HttpPeer {
685    pub _address: SocketAddr,
686    pub scheme: Scheme,
687    pub sni: String,
688    pub proxy: Option<Proxy>,
689    pub client_cert_key: Option<Arc<CertKey>>,
690    /// a custom field to isolate connection reuse. Requests with different group keys
691    /// cannot share connections with each other.
692    pub group_key: u64,
693    pub options: PeerOptions,
694}
695
696impl HttpPeer {
697    // These methods are pretty ad-hoc
698    pub fn is_tls(&self) -> bool {
699        match self.scheme {
700            Scheme::HTTP => false,
701            Scheme::HTTPS => true,
702        }
703    }
704
705    fn new_from_sockaddr(address: SocketAddr, tls: bool, sni: String) -> Self {
706        HttpPeer {
707            _address: address,
708            scheme: Scheme::from_tls_bool(tls),
709            sni,
710            proxy: None,
711            client_cert_key: None,
712            group_key: 0,
713            options: PeerOptions::new(),
714        }
715    }
716
717    /// Create a new [`HttpPeer`] with the given socket address and TLS settings.
718    pub fn new<A: ToInetSocketAddrs>(address: A, tls: bool, sni: String) -> Self {
719        let mut addrs_iter = address.to_socket_addrs().unwrap(); //TODO: handle error
720        let addr = addrs_iter.next().unwrap();
721        Self::new_from_sockaddr(SocketAddr::Inet(addr), tls, sni)
722    }
723
724    /// Create a new [`HttpPeer`] with the given path to Unix domain socket and TLS settings.
725    #[cfg(unix)]
726    pub fn new_uds(path: &str, tls: bool, sni: String) -> Result<Self> {
727        let addr = SocketAddr::Unix(
728            UnixSocketAddr::from_pathname(Path::new(path)).or_err(SocketError, "invalid path")?,
729        );
730        Ok(Self::new_from_sockaddr(addr, tls, sni))
731    }
732
733    /// Create a new [`HttpPeer`] that uses a proxy to connect to the upstream IP and port
734    /// combination.
735    pub fn new_proxy(
736        next_hop: &str,
737        ip_addr: IpAddr,
738        port: u16,
739        tls: bool,
740        sni: &str,
741        headers: BTreeMap<String, Vec<u8>>,
742    ) -> Self {
743        HttpPeer {
744            _address: SocketAddr::Inet(InetSocketAddr::new(ip_addr, port)),
745            scheme: Scheme::from_tls_bool(tls),
746            sni: sni.to_string(),
747            proxy: Some(Proxy {
748                next_hop: PathBuf::from(next_hop).into(),
749                host: ip_addr.to_string(),
750                port,
751                headers,
752            }),
753            client_cert_key: None,
754            group_key: 0,
755            options: PeerOptions::new(),
756        }
757    }
758
759    /// Create a new [`HttpPeer`] with client certificate and key for mutual TLS.
760    pub fn new_mtls<A: ToInetSocketAddrs>(
761        address: A,
762        sni: String,
763        client_cert_key: Arc<CertKey>,
764    ) -> Self {
765        let mut peer = Self::new(address, true, sni);
766        peer.client_cert_key = Some(client_cert_key);
767        peer
768    }
769
770    fn peer_hash(&self) -> u64 {
771        let mut hasher = AHasher::default();
772        self.hash(&mut hasher);
773        hasher.finish()
774    }
775}
776
777impl Hash for HttpPeer {
778    fn hash<H: Hasher>(&self, state: &mut H) {
779        self._address.hash(state);
780        self.scheme.hash(state);
781        self.proxy.hash(state);
782        self.sni.hash(state);
783        // client cert serial
784        self.client_cert_key.hash(state);
785        // origin server cert verification
786        self.verify_cert().hash(state);
787        self.verify_hostname().hash(state);
788        self.alternative_cn().hash(state);
789        #[cfg(feature = "s2n")]
790        self.get_psk().hash(state);
791        self.group_key.hash(state);
792        // max h2 stream settings
793        self.options.max_h2_streams.hash(state);
794        // h2_stream_window_size and h2_connection_window_size are intentionally excluded
795        // from the reuse hash for now. These are per-connection settings applied at handshake
796        // time and may be revisited alongside other h2 settings that could be dynamically
797        // adjusted over the lifetime of a connection.
798        self.options.curves.hash(state);
799        self.options.second_keyshare.hash(state);
800    }
801}
802
803impl Display for HttpPeer {
804    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
805        write!(f, "addr: {}, scheme: {}", self._address, self.scheme)?;
806        if !self.sni.is_empty() {
807            write!(f, ", sni: {}", self.sni)?;
808        }
809        if let Some(p) = self.proxy.as_ref() {
810            write!(f, ", proxy: {p}")?;
811        }
812        if let Some(cert) = &self.client_cert_key {
813            write!(f, ", client cert: {}", cert)?;
814        }
815        Ok(())
816    }
817}
818
819impl Peer for HttpPeer {
820    fn address(&self) -> &SocketAddr {
821        &self._address
822    }
823
824    fn tls(&self) -> bool {
825        self.is_tls()
826    }
827
828    fn sni(&self) -> &str {
829        &self.sni
830    }
831
832    // TODO: change connection pool to accept u64 instead of String
833    fn reuse_hash(&self) -> u64 {
834        self.peer_hash()
835    }
836
837    fn get_peer_options(&self) -> Option<&PeerOptions> {
838        Some(&self.options)
839    }
840
841    fn get_mut_peer_options(&mut self) -> Option<&mut PeerOptions> {
842        Some(&mut self.options)
843    }
844
845    fn get_proxy(&self) -> Option<&Proxy> {
846        self.proxy.as_ref()
847    }
848
849    #[cfg(unix)]
850    fn matches_fd<V: AsRawFd>(&self, fd: V) -> bool {
851        if let Some(proxy) = self.get_proxy() {
852            proxy.next_hop.check_fd_match(fd)
853        } else {
854            self.address().check_fd_match(fd)
855        }
856    }
857
858    #[cfg(windows)]
859    fn matches_sock<V: AsRawSocket>(&self, sock: V) -> bool {
860        use crate::protocols::ConnSockReusable;
861
862        if let Some(proxy) = self.get_proxy() {
863            panic!("windows do not support peers with proxy")
864        } else {
865            self.address().check_sock_match(sock)
866        }
867    }
868
869    fn get_client_cert_key(&self) -> Option<&Arc<CertKey>> {
870        self.client_cert_key.as_ref()
871    }
872
873    fn get_tracer(&self) -> Option<Tracer> {
874        self.options.tracer.clone()
875    }
876}
877
878/// The proxy settings to connect to the remote server, CONNECT only for now
879#[derive(Debug, Hash, Clone)]
880pub struct Proxy {
881    pub next_hop: Box<Path>, // for now this will be the path to the UDS
882    pub host: String,        // the proxied host. Could be either IP addr or hostname.
883    pub port: u16,           // the port to proxy to
884    pub headers: BTreeMap<String, Vec<u8>>, // the additional headers to add to CONNECT
885}
886
887impl Display for Proxy {
888    fn fmt(&self, f: &mut Formatter) -> FmtResult {
889        write!(
890            f,
891            "next_hop: {}, host: {}, port: {}",
892            self.next_hop.display(),
893            self.host,
894            self.port
895        )
896    }
897}
898
899#[cfg(test)]
900mod tests {
901    use super::*;
902
903    #[test]
904    fn default_http_upstream_request_policy_is_standards_oriented() {
905        let policy = PeerOptions::new().http_upstream_request_policy;
906        assert!(policy.strip_hop_by_hop);
907        assert!(policy.strip_connection_nominated);
908        assert!(policy.reject_malformed_connection_nominations);
909        assert_eq!(policy.h1_upgrade, H1UpgradePolicy::WebSocketOnly);
910    }
911
912    #[test]
913    fn preserve_http_upstream_request_policy_is_a_legacy_preset() {
914        let policy = HttpUpstreamRequestPolicy::preserve();
915        assert!(!policy.strip_hop_by_hop);
916        assert!(!policy.strip_connection_nominated);
917        assert!(!policy.reject_malformed_connection_nominations);
918        assert_eq!(policy.h1_upgrade, H1UpgradePolicy::Preserve);
919    }
920}