Skip to main content

dynamo_runtime/utils/
ip_resolver.rs

1// SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! IP resolution utilities for getting local IP addresses with fallback support
5
6use crate::pipeline::network::tcp::server::{DefaultIpResolver, IpResolver};
7use local_ip_address::Error;
8use std::net::IpAddr;
9
10fn resolve_local_ip_with_resolver<R: IpResolver>(resolver: R) -> IpAddr {
11    let resolved_ip = resolver.local_ip().or_else(|err| match err {
12        Error::LocalIpAddressNotFound => resolver.local_ipv6(),
13        _ => Err(err),
14    });
15
16    match resolved_ip {
17        Ok(addr) => addr,
18        Err(Error::LocalIpAddressNotFound) => IpAddr::from([127, 0, 0, 1]),
19        Err(_) => IpAddr::from([127, 0, 0, 1]), // Fallback for any other error
20    }
21}
22
23fn format_ip_for_url(addr: IpAddr) -> String {
24    // Wrap IPv6 addresses with brackets for safe URL construction
25    // e.g., "2001:db8::1" becomes "[2001:db8::1]" so that "{host}:{port}" is valid
26    match addr {
27        IpAddr::V6(_) => format!("[{}]", addr),
28        IpAddr::V4(_) => addr.to_string(),
29    }
30}
31
32/// Get the local IP address for advertising endpoints, using IpResolver with fallback to 127.0.0.1
33///
34/// This function attempts to resolve the local IP address using the provided resolver.
35/// If resolution fails, it falls back to 127.0.0.1 (localhost).
36///
37/// IPv6 addresses are wrapped with brackets for safe URL construction (e.g., `[::1]`).
38///
39/// # Arguments
40/// * `resolver` - An implementation of IpResolver trait for getting local IP addresses
41///
42/// # Returns
43/// A string representation of the resolved IP address (IPv6 addresses are bracketed)
44pub fn get_local_ip_for_advertise_with_resolver<R: IpResolver>(resolver: R) -> String {
45    format_ip_for_url(resolve_local_ip_with_resolver(resolver))
46}
47
48/// Get the local IP address for advertising endpoints using the default resolver.
49pub fn get_local_ip_for_advertise() -> String {
50    get_local_ip_for_advertise_with_resolver(DefaultIpResolver)
51}
52
53/// Get the local IP address for HTTP RPC host binding, using IpResolver with fallback to 127.0.0.1
54pub fn get_http_rpc_host_with_resolver<R: IpResolver>(resolver: R) -> String {
55    get_local_ip_for_advertise_with_resolver(resolver)
56}
57
58/// Get the local IP address for HTTP RPC host binding using the default resolver
59///
60/// This is a convenience function that uses the DefaultIpResolver.
61/// It follows the same logic as the TcpStreamServer for IP resolution.
62///
63/// # Returns
64/// A string representation of the resolved IP address, with fallback to "127.0.0.1"
65pub fn get_http_rpc_host() -> String {
66    get_http_rpc_host_with_resolver(DefaultIpResolver)
67}
68
69/// Get the HTTP RPC host from environment variable or resolve local IP as fallback
70///
71/// This function checks the DYN_HTTP_RPC_HOST environment variable first.
72/// If not set, it uses IP resolution to determine the local IP address.
73///
74/// # Returns
75/// A string representation of the HTTP RPC host address
76pub fn get_http_rpc_host_from_env() -> String {
77    std::env::var("DYN_HTTP_RPC_HOST").unwrap_or_else(|_| get_http_rpc_host())
78}
79
80/// Get the TCP RPC host from environment variable or resolve local IP as fallback
81///
82/// This function checks the DYN_TCP_RPC_HOST environment variable first.
83/// If not set, it uses IP resolution to determine the local IP address.
84///
85/// # Returns
86/// A string representation of the TCP RPC host address
87pub fn get_tcp_rpc_host_from_env() -> String {
88    std::env::var("DYN_TCP_RPC_HOST").unwrap_or_else(|_| get_http_rpc_host())
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94    use local_ip_address::Error;
95
96    // Mock resolver for testing
97    struct MockIpResolver {
98        ipv4_result: Result<IpAddr, Error>,
99        ipv6_result: Result<IpAddr, Error>,
100    }
101
102    impl IpResolver for MockIpResolver {
103        fn local_ip(&self) -> Result<IpAddr, Error> {
104            match &self.ipv4_result {
105                Ok(addr) => Ok(*addr),
106                Err(Error::LocalIpAddressNotFound) => Err(Error::LocalIpAddressNotFound),
107                Err(_) => Err(Error::LocalIpAddressNotFound), // Simplify for testing
108            }
109        }
110
111        fn local_ipv6(&self) -> Result<IpAddr, Error> {
112            match &self.ipv6_result {
113                Ok(addr) => Ok(*addr),
114                Err(Error::LocalIpAddressNotFound) => Err(Error::LocalIpAddressNotFound),
115                Err(_) => Err(Error::LocalIpAddressNotFound), // Simplify for testing
116            }
117        }
118    }
119
120    #[test]
121    fn test_get_http_rpc_host_with_successful_ipv4() {
122        let resolver = MockIpResolver {
123            ipv4_result: Ok(IpAddr::from([192, 168, 1, 100])),
124            ipv6_result: Ok(IpAddr::from([0, 0, 0, 0, 0, 0, 0, 1])),
125        };
126
127        let result = get_http_rpc_host_with_resolver(resolver);
128        assert_eq!(result, "192.168.1.100");
129    }
130
131    #[test]
132    fn test_get_http_rpc_host_with_ipv4_fail_ipv6_success() {
133        let resolver = MockIpResolver {
134            ipv4_result: Err(Error::LocalIpAddressNotFound),
135            ipv6_result: Ok(IpAddr::from([0x2001, 0xdb8, 0, 0, 0, 0, 0, 1])),
136        };
137
138        let result = get_http_rpc_host_with_resolver(resolver);
139        // IPv6 addresses should be bracketed for safe URL construction
140        assert_eq!(result, "[2001:db8::1]");
141    }
142
143    #[test]
144    fn test_get_http_rpc_host_with_both_fail() {
145        let resolver = MockIpResolver {
146            ipv4_result: Err(Error::LocalIpAddressNotFound),
147            ipv6_result: Err(Error::LocalIpAddressNotFound),
148        };
149
150        let result = get_http_rpc_host_with_resolver(resolver);
151        assert_eq!(result, "127.0.0.1");
152    }
153
154    #[test]
155    fn test_get_http_rpc_host_from_env_with_env_var() {
156        // Set environment variable
157        unsafe {
158            std::env::set_var("DYN_HTTP_RPC_HOST", "10.0.0.1");
159        }
160
161        let result = get_http_rpc_host_from_env();
162        assert_eq!(result, "10.0.0.1");
163
164        // Clean up
165        unsafe {
166            std::env::remove_var("DYN_HTTP_RPC_HOST");
167        }
168    }
169
170    #[test]
171    fn test_get_http_rpc_host_from_env_without_env_var() {
172        // Note: We can't reliably unset environment variables in tests
173        // This test assumes DYN_HTTP_RPC_HOST is not set to a specific test value
174
175        let result = get_http_rpc_host_from_env();
176        // Should return some IP address (either resolved or fallback)
177        assert!(!result.is_empty());
178
179        // Should be a valid IP address (strip brackets for IPv6 before parsing)
180        let ip_str = result.trim_start_matches('[').trim_end_matches(']');
181        let _: IpAddr = ip_str.parse().expect("Should be a valid IP address");
182    }
183
184    #[test]
185    fn test_ipv6_address_is_bracketed() {
186        let resolver = MockIpResolver {
187            ipv4_result: Err(Error::LocalIpAddressNotFound),
188            ipv6_result: Ok(IpAddr::from([0xfd00, 0xdead, 0xbeef, 0, 0, 0, 0, 2])),
189        };
190
191        let result = get_http_rpc_host_with_resolver(resolver);
192        // IPv6 must be bracketed for URLs like http://{host}:{port}/path
193        assert!(result.starts_with('['), "IPv6 should start with '['");
194        assert!(result.ends_with(']'), "IPv6 should end with ']'");
195        assert_eq!(result, "[fd00:dead:beef::2]");
196    }
197
198    #[test]
199    fn test_ipv4_address_not_bracketed() {
200        let resolver = MockIpResolver {
201            ipv4_result: Ok(IpAddr::from([10, 0, 0, 1])),
202            ipv6_result: Err(Error::LocalIpAddressNotFound),
203        };
204
205        let result = get_http_rpc_host_with_resolver(resolver);
206        // IPv4 should NOT be bracketed
207        assert!(!result.contains('['), "IPv4 should not contain '['");
208        assert_eq!(result, "10.0.0.1");
209    }
210}