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