Skip to main content

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}