@@ -158,6 +158,52 @@ pub fn dest_name(dest: &str) -> &str {
158158 dest. strip_prefix ( "sip:" ) . unwrap_or ( dest)
159159}
160160
161+ /// Extract port from a normalized (sip: stripped) target like `host:port`.
162+ /// Returns `None` if no port is present.
163+ fn extract_port ( target : & str ) -> Option < String > {
164+ let target = target. trim ( ) ;
165+ if let Some ( pos) = target. rfind ( ':' ) {
166+ let port_str = & target[ pos + 1 ..] ;
167+ if port_str. parse :: < u16 > ( ) . is_ok ( ) {
168+ return Some ( port_str. to_string ( ) ) ;
169+ }
170+ }
171+ None
172+ }
173+
174+ /// Build a deduplicated list of probe targets for per-IP mode.
175+ /// Each inbound_host inherits the port from `dest` if it does not already have one.
176+ /// Duplicate (host:port) combinations are removed.
177+ pub fn build_per_ip_targets ( dest : & str , inbound_hosts : & [ String ] ) -> Vec < String > {
178+ let dest_clean = dest_name ( dest) . to_string ( ) ;
179+ let default_port = extract_port ( & dest_clean) ;
180+
181+ let mut seen = std:: collections:: HashSet :: new ( ) ;
182+ let mut targets = Vec :: new ( ) ;
183+
184+ let add_target = |t : & str , targets : & mut Vec < String > , seen : & mut std:: collections:: HashSet < String > | {
185+ if seen. insert ( t. to_string ( ) ) {
186+ targets. push ( t. to_string ( ) ) ;
187+ }
188+ } ;
189+
190+ add_target ( & dest_clean, & mut targets, & mut seen) ;
191+
192+ for host in inbound_hosts {
193+ let host = host. trim ( ) ;
194+ if extract_port ( host) . is_some ( ) {
195+ add_target ( host, & mut targets, & mut seen) ;
196+ } else if let Some ( ref port) = default_port {
197+ let normalized = format ! ( "{}:{}" , host, port) ;
198+ add_target ( & normalized, & mut targets, & mut seen) ;
199+ } else {
200+ add_target ( host, & mut targets, & mut seen) ;
201+ }
202+ }
203+
204+ targets
205+ }
206+
161207/// Spawn the background health-check loop.
162208///
163209/// It reads `trunks_fn` (which returns the current trunk map) on each tick,
@@ -219,13 +265,11 @@ fn spawn_health_loop_with_timeout(
219265 let has_hosts = !cfg. inbound_hosts . is_empty ( ) ;
220266
221267 if per_ip && has_hosts {
222- // ── Per-IP mode: probe each inbound_host individually.
223- // Dest is probed as part of the targets along with each inbound host
224- // so we can report per-peer health. Trunk is healthy only when ALL
225- // targets are healthy (AND logic), since each peer should be reachable.
226- let mut targets: Vec < String > = Vec :: with_capacity ( 1 + cfg. inbound_hosts . len ( ) ) ;
227- targets. push ( cfg. dest . clone ( ) ) ;
228- targets. extend ( cfg. inbound_hosts . clone ( ) ) ;
268+ // ── Per-IP mode: probe each unique (host:port) target.
269+ // Inbound hosts inherit the port from dest if they don't have one.
270+ // Duplicates are removed: if dest and inbound_host resolve to the
271+ // same host:port, only one probe is sent.
272+ let targets = build_per_ip_targets ( & cfg. dest , & cfg. inbound_hosts ) ;
229273
230274 let mut per_ip_results: Vec < PerIpHealthState > =
231275 Vec :: with_capacity ( targets. len ( ) ) ;
0 commit comments