1use std::collections::HashSet;
16use std::net::SocketAddr;
17use std::sync::Arc;
18
19use anyhow::{Context, Result, bail};
20use oxdock_net_plugin::{BindingSpec, EndpointRegistry, VirtualEndpoint, parse_virtual_endpoint};
21
22#[derive(Debug, Clone, Default)]
25pub struct EndpointFlags {
26 pub listens: Vec<SocketAddr>,
28 pub publishes: Vec<(SocketAddr, VirtualEndpoint)>,
32 pub offline: bool,
34}
35
36pub fn parse_listen_arg(raw: &str) -> Result<SocketAddr> {
41 let text = raw.trim();
42 if text.is_empty() {
43 bail!("--listen requires an address ([host:]port)");
44 }
45 if text.chars().all(|c| c.is_ascii_digit()) {
46 let port: u16 = text
47 .parse()
48 .map_err(|_| anyhow::anyhow!("--listen port must be numeric 1-65535, got {raw:?}"))?;
49 if port == 0 {
50 bail!(
51 "--listen port 0 has no stable service key (use -p 0:<endpoint> for an ephemeral outer port)"
52 );
53 }
54 return Ok(SocketAddr::from(([127, 0, 0, 1], port)));
55 }
56 let addr: SocketAddr = text.parse().with_context(|| {
57 format!("--listen address must be [host:]port (try 0.0.0.0:2251), got {raw:?}")
58 })?;
59 if addr.port() == 0 {
60 bail!(
61 "--listen port 0 has no stable service key (use -p 0:<endpoint> for an ephemeral outer port)"
62 );
63 }
64 Ok(addr)
65}
66
67pub fn parse_publish_arg(raw: &str) -> Result<(SocketAddr, VirtualEndpoint)> {
74 let text = raw.trim();
75 let Some((outer_text, inner_text)) = text.rsplit_once(':') else {
76 bail!(
77 "-p requires [host:]outer:inner (try -p 2222:2251 or -p 127.0.0.1:1234:ssh-server), got {raw:?}"
78 );
79 };
80 let inner = parse_virtual_endpoint(inner_text.trim(), "-p")
81 .with_context(|| format!("-p inner endpoint invalid in {raw:?}"))?;
82 let outer_text = outer_text.trim();
83 let addr = if let Ok(port) = outer_text.parse::<u16>() {
84 SocketAddr::from(([0, 0, 0, 0], port))
85 } else if let Ok(addr) = outer_text.parse::<SocketAddr>() {
86 addr
87 } else {
88 bail!(
89 "-p outer address invalid in {raw:?} (expected a port 0-65535 or a [host:]port address)"
90 );
91 };
92 Ok((addr, inner))
93}
94
95pub fn build_registry(flags: &EndpointFlags) -> Result<Arc<EndpointRegistry>> {
99 if flags.offline && (!flags.listens.is_empty() || !flags.publishes.is_empty()) {
100 bail!("--offline conflicts with --listen/-p (offline runs open no sockets)");
101 }
102 let registry = Arc::new(EndpointRegistry::new(flags.offline));
103 let mut inners: HashSet<String> = HashSet::new();
104 let mut outers: HashSet<SocketAddr> = HashSet::new();
105 for addr in &flags.listens {
106 let endpoint = VirtualEndpoint::Port(addr.port());
107 if !inners.insert(endpoint.key()) {
108 bail!("virtual endpoint '{endpoint}' is mapped twice");
109 }
110 if addr.port() != 0 && !outers.insert(*addr) {
111 bail!("outer socket address {addr} is mapped twice");
112 }
113 registry.add_mapping(&endpoint, BindingSpec::Exposed { addr: *addr })?;
114 }
115 for (addr, inner) in &flags.publishes {
116 if !inners.insert(inner.key()) {
117 bail!("virtual endpoint '{inner}' is mapped twice");
118 }
119 if addr.port() != 0 && !outers.insert(*addr) {
120 bail!("outer socket address {addr} is mapped twice");
121 }
122 registry.add_mapping(inner, BindingSpec::Exposed { addr: *addr })?;
123 }
124 registry.bind_all()?;
125 Ok(registry)
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 #[test]
133 fn listen_forms() {
134 assert_eq!(
135 parse_listen_arg("2251").unwrap(),
136 SocketAddr::from(([127, 0, 0, 1], 2251))
137 );
138 assert_eq!(
139 parse_listen_arg("0.0.0.0:2251").unwrap(),
140 SocketAddr::from(([0, 0, 0, 0], 2251))
141 );
142 for bad in ["", "0", "2251:0", "0.0.0.0:0", "not-an-addr"] {
143 parse_listen_arg(bad).expect_err("bad listen must fail");
144 }
145 }
146
147 #[test]
148 fn publish_forms() {
149 assert_eq!(
150 parse_publish_arg("2222:2251").unwrap(),
151 (
152 SocketAddr::from(([0, 0, 0, 0], 2222)),
153 VirtualEndpoint::Port(2251)
154 )
155 );
156 assert_eq!(
157 parse_publish_arg("0:demo-proxy").unwrap(),
158 (
159 SocketAddr::from(([0, 0, 0, 0], 0)),
160 VirtualEndpoint::Name("demo-proxy".to_string())
161 )
162 );
163 for bad in ["", "2251", "abc:2251", "2222:0", "2222:127.0.0.1:2251"] {
164 parse_publish_arg(bad).expect_err("bad publish must fail");
165 }
166 }
167
168 #[test]
169 fn publish_three_part_host_port_forms() {
170 assert_eq!(
171 parse_publish_arg("127.0.0.1:1234:ssh-server").unwrap(),
172 (
173 "127.0.0.1:1234".parse().unwrap(),
174 VirtualEndpoint::Name("ssh-server".to_string())
175 )
176 );
177 assert_eq!(
178 parse_publish_arg("127.0.0.1:1234:2251").unwrap(),
179 (
180 "127.0.0.1:1234".parse().unwrap(),
181 VirtualEndpoint::Port(2251)
182 )
183 );
184 assert_eq!(
185 parse_publish_arg("[::1]:8080:web").unwrap(),
186 (
187 "[::1]:8080".parse().unwrap(),
188 VirtualEndpoint::Name("web".to_string())
189 )
190 );
191 }
192
193 #[test]
194 fn registry_rejects_conflicts() {
195 let flags = EndpointFlags {
196 offline: true,
197 listens: vec![SocketAddr::from(([0, 0, 0, 0], 2251))],
198 ..EndpointFlags::default()
199 };
200 build_registry(&flags).expect_err("offline plus listen must fail");
201 let flags = EndpointFlags {
202 publishes: vec![
203 (
204 SocketAddr::from(([0, 0, 0, 0], 2222)),
205 VirtualEndpoint::Port(2251),
206 ),
207 (
208 SocketAddr::from(([0, 0, 0, 0], 2223)),
209 VirtualEndpoint::Port(2251),
210 ),
211 ],
212 ..EndpointFlags::default()
213 };
214 build_registry(&flags).expect_err("duplicate inner must fail");
215 let flags = EndpointFlags {
216 publishes: vec![
217 (
218 SocketAddr::from(([0, 0, 0, 0], 2222)),
219 VirtualEndpoint::Port(2251),
220 ),
221 (
222 SocketAddr::from(([0, 0, 0, 0], 2222)),
223 VirtualEndpoint::Port(2252),
224 ),
225 ],
226 ..EndpointFlags::default()
227 };
228 build_registry(&flags).expect_err("duplicate outer must fail");
229 }
230}