Skip to main content

eggress_runtime/
reverse.rs

1//! Runtime-side adapter that wires the `eggress-routing` engine into the
2//! `eggress-protocol-reverse` client.
3//!
4//! The reverse client dials a fixed external host:port and forwards the
5//! resulting connections back over the control channel. The runtime is
6//! still in charge of authorization: each reconnect consults the router
7//! against a synthetic `RouteRequest` (transport = `ReverseTcp`, listener
8//! = the configured reverse listener name, target = the external target)
9//! and rejects the connection with a structured reason when the policy
10//! says no.
11//!
12//! The adapter implements the `TargetResolver` trait from the protocol
13//! crate without requiring the protocol crate to depend on the routing
14//! crate directly.
15use std::net::SocketAddr;
16use std::sync::Arc;
17
18use eggress_core::{ClientIdentity, TargetAddr, TargetHost};
19use eggress_protocol_reverse::client::{TargetResolution, TargetResolver};
20use eggress_routing::{
21    RouteDecision, RouteRequest, RouteService, SharedRoutingService, TransportKind,
22};
23use tracing::warn;
24
25/// Adapter that gates a fixed external target through a `SharedRoutingService`.
26///
27/// Each call to `resolve()`:
28/// 1. Builds a synthetic `RouteRequest` with `transport = ReverseTcp`,
29///    `listener = reverse_listener_name`, and `target = (host, port)`.
30/// 2. Calls `router.decide()`. If the decision is `Direct` or
31///    `UpstreamGroup`, returns `TargetResolution::Connect`. For reverse
32///    traffic, the routing decision is a gate, not a redirect — the
33///    reverse client always dials the same external target.
34/// 3. If the decision is `Reject`, returns `TargetResolution::Reject`
35///    with the reason.
36pub struct RouteEngineTargetResolver {
37    router: Arc<SharedRoutingService>,
38    target: TargetAddr,
39    reverse_listener: Arc<str>,
40    client_addr: Option<SocketAddr>,
41}
42
43impl RouteEngineTargetResolver {
44    /// Build a new resolver.
45    pub fn new(
46        router: Arc<SharedRoutingService>,
47        host: String,
48        port: u16,
49        reverse_listener: Arc<str>,
50        client_addr: Option<SocketAddr>,
51    ) -> Self {
52        let target = TargetAddr {
53            host: TargetHost::Domain(host),
54            port,
55        };
56        Self {
57            router,
58            target,
59            reverse_listener,
60            client_addr,
61        }
62    }
63
64    /// Borrow the current target. Used by callers that want to display
65    /// the configured target without going through the resolver.
66    pub fn target(&self) -> &TargetAddr {
67        &self.target
68    }
69}
70
71impl TargetResolver for RouteEngineTargetResolver {
72    fn resolve(&self) -> TargetResolution {
73        let request = RouteRequest {
74            target: &self.target,
75            source: self.client_addr,
76            listener: self.reverse_listener.as_ref(),
77            inbound_protocol: eggress_core::ProtocolId::Reverse,
78            identity: &ClientIdentity::Anonymous,
79            transport: TransportKind::ReverseTcp,
80        };
81
82        match self.router.decide(&request) {
83            RouteDecision::Direct { .. } | RouteDecision::UpstreamGroup { .. } => {
84                TargetResolution::Connect {
85                    host: self.target.host.to_string(),
86                    port: self.target.port,
87                }
88            }
89            RouteDecision::Reject { reason, .. } => {
90                warn!(
91                    listener = %self.reverse_listener,
92                    target = %self.target,
93                    ?reason,
94                    "reverse route rejected by policy",
95                );
96                TargetResolution::Reject {
97                    reason: format!("route rejected: {:?}", reason),
98                }
99            }
100        }
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107    use eggress_core::RejectReason;
108    use eggress_routing::{CompiledRule, RouteActionSpec, Router, RuleId, SharedRoutingService};
109
110    fn router_with_default(action: RouteActionSpec) -> Arc<SharedRoutingService> {
111        Arc::new(SharedRoutingService::new(Router::new(Vec::new(), action)))
112    }
113
114    #[test]
115    fn allows_direct_route() {
116        let r = router_with_default(RouteActionSpec::Direct);
117        let resolver = RouteEngineTargetResolver::new(
118            r,
119            "127.0.0.1".to_string(),
120            8080,
121            Arc::from("rev-1"),
122            None,
123        );
124        match resolver.resolve() {
125            TargetResolution::Connect { host, port } => {
126                assert_eq!(host, "127.0.0.1");
127                assert_eq!(port, 8080);
128            }
129            other => panic!("expected Connect, got {:?}", other),
130        }
131    }
132
133    #[test]
134    fn allows_upstream_group_route() {
135        let r = router_with_default(RouteActionSpec::UpstreamGroup(
136            eggress_routing::UpstreamGroupId(Arc::from("group-1")),
137        ));
138        let resolver = RouteEngineTargetResolver::new(
139            r,
140            "127.0.0.1".to_string(),
141            9090,
142            Arc::from("rev-1"),
143            None,
144        );
145        // UpstreamGroup decision still maps to Connect for the reverse client
146        // — routing is a gate, not a redirect.
147        match resolver.resolve() {
148            TargetResolution::Connect { port, .. } => assert_eq!(port, 9090),
149            other => panic!("expected Connect, got {:?}", other),
150        }
151    }
152
153    #[test]
154    fn rejects_reject_route() {
155        let rule = CompiledRule {
156            id: RuleId(Arc::from("deny-all")),
157            matcher: eggress_routing::MatchExpr::Any,
158            action: RouteActionSpec::Reject(RejectReason::AccessDenied),
159        };
160        let router = Arc::new(SharedRoutingService::new(Router::new(
161            vec![rule],
162            RouteActionSpec::Direct,
163        )));
164        let resolver = RouteEngineTargetResolver::new(
165            router,
166            "127.0.0.1".to_string(),
167            80,
168            Arc::from("rev-1"),
169            None,
170        );
171        match resolver.resolve() {
172            TargetResolution::Reject { reason } => {
173                assert!(reason.contains("route rejected"), "reason: {}", reason);
174            }
175            other => panic!("expected Reject, got {:?}", other),
176        }
177    }
178
179    #[test]
180    fn reverse_listener_matcher_only_matches_reverse_tcp() {
181        // Build a rule that matches reverse listener "rev-1"
182        let rule = CompiledRule {
183            id: RuleId(Arc::from("reverse-only")),
184            matcher: eggress_routing::MatchExpr::ReverseListener(Arc::from("rev-1")),
185            action: RouteActionSpec::Direct,
186        };
187        let router = Arc::new(SharedRoutingService::new(Router::new(
188            vec![rule],
189            RouteActionSpec::Reject(RejectReason::AccessDenied),
190        )));
191        let resolver = RouteEngineTargetResolver::new(
192            router,
193            "127.0.0.1".to_string(),
194            8080,
195            Arc::from("rev-1"),
196            None,
197        );
198        // The reverse listener matcher should match; default falls through to it
199        assert!(matches!(
200            resolver.resolve(),
201            TargetResolution::Connect { .. }
202        ));
203    }
204}