epics_libcom_rs/runtime/net.rs
1use std::net::SocketAddr;
2
3use super::env_table::{
4 EPICS_CA_MCAST_TTL, EPICS_CA_REPEATER_PORT, EPICS_CA_SERVER_PORT, EPICS_CAS_BEACON_PORT,
5 EPICS_CAS_SERVER_PORT,
6};
7
8/// C `CA_SERVER_PORT` (`caProto.h`) — const-derived from the generated
9/// `ENV_PARAM` table, so it cannot drift from `configure/CONFIG_ENV`. A default
10/// that stopped being a valid port would fail const-evaluation.
11pub const CA_SERVER_PORT: u16 = EPICS_CA_SERVER_PORT.default_port();
12/// C `CA_REPEATER_PORT` — see [`CA_SERVER_PORT`].
13pub const CA_REPEATER_PORT: u16 = EPICS_CA_REPEATER_PORT.default_port();
14// PVA port constants (originally from pva/protocol.rs, now in epics-pva-rs).
15// NOT const-derived: PVA has no `ENV_PARAM` in EPICS Base — pvxs owns its own
16// config vocabulary, and `EPICS_PVA_SERVER_PORT` is not in `env_param_list[]`.
17pub const PVA_SERVER_PORT: u16 = 5075;
18pub const PVA_BROADCAST_PORT: u16 = 5076;
19
20/// Client-side CA server-port reader — where to **send** SEARCH.
21/// Honours `EPICS_CA_SERVER_PORT` only; the server-only
22/// `EPICS_CAS_SERVER_PORT` does **not** affect client behaviour.
23///
24/// C parity: matches the client path's `envGetInetPortConfigParam
25/// (&EPICS_CA_SERVER_PORT, CA_SERVER_PORT)` — the server-specific
26/// variable is intentionally invisible to clients so a process that
27/// hosts both a client and a server (e.g. a gateway) does not get
28/// its client routing redirected by a server-side override.
29pub fn ca_server_port() -> u16 {
30 EPICS_CA_SERVER_PORT.inet_port(CA_SERVER_PORT)
31}
32
33/// Server-side CA bind-port reader — where the server **binds** the
34/// UDP discovery socket and the TCP listener (same value for both,
35/// matching `caservertask.c:491-499` —
36/// `ca_udp_port = ca_server_port`).
37///
38/// Precedence: `EPICS_CAS_SERVER_PORT` > `EPICS_CA_SERVER_PORT` >
39/// [`CA_SERVER_PORT`] (5064). Mirrors C
40/// `caservertask.c::ca_initialize` exactly — the CAS-specific
41/// variable lets a gateway-style process override the *server-side*
42/// port without disturbing its client routing.
43///
44/// For the multi-IOC "shared UDP search port, distinct TCP ports"
45/// deployment, callers should use `epics_base_rs::server::ioc_app::
46/// IocApplication::tcp_port` (or the equivalent `CaServerBuilder::tcp_port`,
47/// in `epics-ca-rs`) to override the
48/// TCP port *only*. The env-var path keeps strict C parity so a
49/// startup script that sets `EPICS_CAS_SERVER_PORT=6064` lands UDP
50/// and TCP on the same port, as it would under a C IOC.
51///
52/// The selection between the two variables is a *presence* test, as in
53/// C: whichever parameter is configured is then resolved against the
54/// **compiled** [`CA_SERVER_PORT`] default, so a rejected
55/// `EPICS_CAS_SERVER_PORT` lands on 5064 — it does not fall through to
56/// `EPICS_CA_SERVER_PORT`.
57pub fn cas_server_port() -> u16 {
58 if EPICS_CAS_SERVER_PORT.get().is_some() {
59 EPICS_CAS_SERVER_PORT.inet_port(CA_SERVER_PORT)
60 } else {
61 EPICS_CA_SERVER_PORT.inet_port(CA_SERVER_PORT)
62 }
63}
64
65/// Returns the CA repeater port, allowing override via `EPICS_CA_REPEATER_PORT`.
66pub fn ca_repeater_port() -> u16 {
67 EPICS_CA_REPEATER_PORT.inet_port(CA_REPEATER_PORT)
68}
69
70/// Server-side beacon port — C `caservertask.c:501-508`.
71///
72/// Presence-gated exactly like [`cas_server_port`]: `EPICS_CAS_BEACON_PORT`
73/// when it is configured, else `EPICS_CA_REPEATER_PORT`, each resolved
74/// against the compiled [`CA_REPEATER_PORT`] default.
75pub fn cas_beacon_port() -> u16 {
76 if EPICS_CAS_BEACON_PORT.get().is_some() {
77 EPICS_CAS_BEACON_PORT.inet_port(CA_REPEATER_PORT)
78 } else {
79 EPICS_CA_REPEATER_PORT.inet_port(CA_REPEATER_PORT)
80 }
81}
82
83/// IP TTL applied to CA multicast traffic.
84///
85/// Reads `EPICS_CA_MCAST_TTL` (epics-base 3.16, commit f2a1834d) and
86/// returns the value clamped to `1..=255` — the protocol field is one
87/// byte and 0 would silently drop every packet at the source NIC.
88/// Default 1 matches both the upstream default and the link-local
89/// scope assumption EPICS clients rely on; raise it only when a
90/// site uses multicast for beacons/search across routed segments.
91///
92/// Applied on a UDP socket via `socket.set_multicast_ttl_v4(value)`.
93/// Has no effect on unicast or limited-broadcast destinations — the
94/// OS only consults this field when the destination is in the
95/// 224.0.0.0/4 range.
96pub fn ca_mcast_ttl() -> u32 {
97 // C `udpiiu.cpp:198-200` / `caservertask.c:324-326`:
98 // long val;
99 // if (envGetLongConfigParam(&EPICS_CA_MCAST_TTL, &val)) val = 1;
100 // — the `1` being a hand-copy of the table default, which here comes from
101 // the table.
102 let ttl = EPICS_CA_MCAST_TTL.long_or_default();
103 u32::try_from(ttl)
104 .ok()
105 .filter(|v| (1..=255).contains(v))
106 .unwrap_or(1)
107}
108
109// There is deliberately NO `pva_server_port()` / `pva_broadcast_port()` here.
110//
111// A PVA port is pvxs's to define, and pvxs's rules are not C's: `PickOne` reads
112// `EPICS_PVAS_SERVER_PORT` before `EPICS_PVA_SERVER_PORT` (`config.cpp:402-408`)
113// and `EPICS_PVA_SERVER_PORT=0` is a legitimate ephemeral-bind request
114// (`config.cpp:624-632`), where `envGetInetPortConfigParam` — the function every
115// helper in THIS module implements — rejects anything at or below 5000 and
116// prints two CA diagnostics saying so. Helpers that looked like the CA ones and
117// silently applied the CA rule to a PVA variable are a trap for the next caller;
118// `epics_pva_rs::config::env` is the owner, and the only owner, of these two.
119
120/// Parse a `"host:port"` string into a `SocketAddr`.
121pub fn parse_socket_addr(s: &str) -> Result<SocketAddr, std::net::AddrParseError> {
122 s.parse()
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128 use serial_test::serial;
129
130 #[test]
131 #[serial(epics_env)]
132 fn test_default_ca_server_port() {
133 // Remove env var to ensure default
134 unsafe { std::env::remove_var("EPICS_CA_SERVER_PORT") };
135 assert_eq!(ca_server_port(), 5064);
136 }
137
138 #[test]
139 #[serial(epics_env)]
140 fn test_default_ca_repeater_port() {
141 unsafe { std::env::remove_var("EPICS_CA_REPEATER_PORT") };
142 assert_eq!(ca_repeater_port(), 5065);
143 }
144
145 #[test]
146 #[serial(epics_env)]
147 fn test_ca_server_port_env_override() {
148 unsafe { std::env::set_var("EPICS_CA_SERVER_PORT", "9064") };
149 assert_eq!(ca_server_port(), 9064);
150 unsafe { std::env::remove_var("EPICS_CA_SERVER_PORT") };
151 }
152
153 #[test]
154 #[serial(epics_env)]
155 fn test_cas_server_port_defaults_to_ca_server_port() {
156 unsafe {
157 std::env::remove_var("EPICS_CAS_SERVER_PORT");
158 std::env::remove_var("EPICS_CA_SERVER_PORT");
159 }
160 assert_eq!(cas_server_port(), CA_SERVER_PORT);
161
162 unsafe { std::env::set_var("EPICS_CA_SERVER_PORT", "9064") };
163 assert_eq!(
164 cas_server_port(),
165 9064,
166 "cas_server_port falls back to EPICS_CA_SERVER_PORT when CAS-specific unset"
167 );
168 unsafe { std::env::remove_var("EPICS_CA_SERVER_PORT") };
169 }
170
171 /// The CAS/CA selection is a *presence* test against the compiled
172 /// default (`caservertask.c:491-508`): a configured-but-rejected
173 /// `EPICS_CAS_SERVER_PORT` lands on 5064, it does NOT fall through to
174 /// `EPICS_CA_SERVER_PORT`. Same rule for the beacon port.
175 #[test]
176 #[serial(epics_env)]
177 fn test_cas_port_selection_is_presence_gated() {
178 unsafe {
179 std::env::set_var("EPICS_CA_SERVER_PORT", "6064");
180 std::env::set_var("EPICS_CAS_SERVER_PORT", "3000");
181 }
182 assert_eq!(
183 cas_server_port(),
184 CA_SERVER_PORT,
185 "rejected EPICS_CAS_SERVER_PORT falls back to the compiled default, not to EPICS_CA_SERVER_PORT"
186 );
187 // Empty is "not configured" — the generic variable is resolved.
188 unsafe { std::env::set_var("EPICS_CAS_SERVER_PORT", "") };
189 assert_eq!(cas_server_port(), 6064);
190
191 unsafe {
192 std::env::set_var("EPICS_CA_REPEATER_PORT", "6065");
193 std::env::set_var("EPICS_CAS_BEACON_PORT", "70000");
194 }
195 assert_eq!(
196 cas_beacon_port(),
197 CA_REPEATER_PORT,
198 "rejected EPICS_CAS_BEACON_PORT falls back to the compiled default"
199 );
200 unsafe { std::env::set_var("EPICS_CAS_BEACON_PORT", "6165") };
201 assert_eq!(cas_beacon_port(), 6165);
202 unsafe { std::env::remove_var("EPICS_CAS_BEACON_PORT") };
203 assert_eq!(cas_beacon_port(), 6065);
204
205 unsafe {
206 std::env::remove_var("EPICS_CA_SERVER_PORT");
207 std::env::remove_var("EPICS_CAS_SERVER_PORT");
208 std::env::remove_var("EPICS_CA_REPEATER_PORT");
209 }
210 }
211
212 #[test]
213 #[serial(epics_env)]
214 fn test_ca_mcast_ttl_default() {
215 unsafe { std::env::remove_var("EPICS_CA_MCAST_TTL") };
216 assert_eq!(ca_mcast_ttl(), 1);
217 }
218
219 #[test]
220 #[serial(epics_env)]
221 fn test_ca_mcast_ttl_override() {
222 unsafe { std::env::set_var("EPICS_CA_MCAST_TTL", "32") };
223 assert_eq!(ca_mcast_ttl(), 32);
224 unsafe { std::env::remove_var("EPICS_CA_MCAST_TTL") };
225 }
226
227 #[test]
228 #[serial(epics_env)]
229 fn test_ca_mcast_ttl_clamps_invalid_to_default() {
230 for bad in ["0", "256", "abc", ""] {
231 unsafe { std::env::set_var("EPICS_CA_MCAST_TTL", bad) };
232 assert_eq!(
233 ca_mcast_ttl(),
234 1,
235 "invalid EPICS_CA_MCAST_TTL={bad:?} must fall back to default 1"
236 );
237 }
238 unsafe { std::env::remove_var("EPICS_CA_MCAST_TTL") };
239 }
240
241 #[test]
242 #[serial(epics_env)]
243 fn test_cas_server_port_overrides_ca_server_port() {
244 // C parity regression (caservertask.c:491-499):
245 // ca_server_port = EPICS_CAS_SERVER_PORT (if set) else
246 // EPICS_CA_SERVER_PORT else default 5064
247 // ca_udp_port = ca_server_port <-- UDP follows the same value
248 //
249 // Earlier this fn was named cas_server_port() but documented as
250 // a "TCP-only" override with UDP staying at 5064 — that
251 // diverged from C and broke startup scripts that set
252 // EPICS_CAS_SERVER_PORT expecting both ports to follow.
253 unsafe {
254 std::env::set_var("EPICS_CA_SERVER_PORT", "5064");
255 std::env::set_var("EPICS_CAS_SERVER_PORT", "9064");
256 }
257 // C parity: client semantic only honours EPICS_CA_SERVER_PORT
258 // (`envGetInetPortConfigParam(&EPICS_CA_SERVER_PORT, ...)`).
259 assert_eq!(
260 ca_server_port(),
261 5064,
262 "client semantic ignores EPICS_CAS_SERVER_PORT"
263 );
264 // C parity: server-side bind port honours EPICS_CAS_SERVER_PORT
265 // first (caservertask.c:491). Both UDP and TCP follow this
266 // value unless the caller splits via `.tcp_port(...)`.
267 assert_eq!(
268 cas_server_port(),
269 9064,
270 "server-side bind picks up EPICS_CAS_SERVER_PORT"
271 );
272 unsafe {
273 std::env::remove_var("EPICS_CAS_SERVER_PORT");
274 std::env::remove_var("EPICS_CA_SERVER_PORT");
275 }
276 }
277
278 #[test]
279 #[serial(epics_env)]
280 fn test_ca_server_port_rejects_privileged_port() {
281 // H2 C-parity: a port <= IPPORT_USERRESERVED (5000) is rejected
282 // by `envGetInetPortConfigParam` and falls back to the
283 // compiled default — `EPICS_CA_SERVER_PORT=80` must NOT be
284 // honoured.
285 unsafe { std::env::set_var("EPICS_CA_SERVER_PORT", "80") };
286 assert_eq!(
287 ca_server_port(),
288 CA_SERVER_PORT,
289 "privileged port 80 must fall back to the default 5064"
290 );
291 unsafe { std::env::set_var("EPICS_CA_SERVER_PORT", "5000") };
292 assert_eq!(
293 ca_server_port(),
294 CA_SERVER_PORT,
295 "port 5000 (== IPPORT_USERRESERVED) must fall back to default"
296 );
297 unsafe { std::env::set_var("EPICS_CA_SERVER_PORT", "0") };
298 assert_eq!(ca_server_port(), CA_SERVER_PORT, "port 0 must fall back");
299 unsafe { std::env::remove_var("EPICS_CA_SERVER_PORT") };
300 }
301
302 #[test]
303 #[serial(epics_env)]
304 fn test_ca_server_port_accepts_valid_high_port() {
305 // 5001 is the first acceptable port (> IPPORT_USERRESERVED).
306 unsafe { std::env::set_var("EPICS_CA_SERVER_PORT", "5001") };
307 assert_eq!(ca_server_port(), 5001);
308 unsafe { std::env::set_var("EPICS_CA_SERVER_PORT", "6064") };
309 assert_eq!(ca_server_port(), 6064);
310 unsafe { std::env::remove_var("EPICS_CA_SERVER_PORT") };
311 }
312
313 #[test]
314 #[serial(epics_env)]
315 fn test_ca_repeater_port_rejects_out_of_range() {
316 // H2 applies to every port reader, not just CA server.
317 unsafe { std::env::set_var("EPICS_CA_REPEATER_PORT", "443") };
318 assert_eq!(ca_repeater_port(), CA_REPEATER_PORT);
319 unsafe { std::env::remove_var("EPICS_CA_REPEATER_PORT") };
320 }
321
322 #[test]
323 #[serial(epics_env)]
324 fn test_port_reader_lenient_parse() {
325 // H3 C-parity: `sscanf("%ld")` tolerates leading whitespace and
326 // a trailing garbage suffix that `u16::parse` would reject.
327 unsafe { std::env::set_var("EPICS_CA_SERVER_PORT", " 6064") };
328 assert_eq!(ca_server_port(), 6064);
329 unsafe { std::env::set_var("EPICS_CA_SERVER_PORT", "6064xyz") };
330 assert_eq!(ca_server_port(), 6064);
331 unsafe { std::env::remove_var("EPICS_CA_SERVER_PORT") };
332 }
333
334 #[test]
335 fn test_parse_socket_addr_valid() {
336 let addr = parse_socket_addr("127.0.0.1:5064").unwrap();
337 assert_eq!(addr.port(), 5064);
338 assert_eq!(addr.ip(), std::net::Ipv4Addr::LOCALHOST);
339 }
340
341 #[test]
342 fn test_parse_socket_addr_invalid() {
343 assert!(parse_socket_addr("not-an-address").is_err());
344 }
345}