From 3f67c8b39718953ca15eadd8dbbbdbebd42be708 Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Thu, 7 May 2026 13:19:56 +0200 Subject: [PATCH] Fix route status indicator for dynamic (DNS) routes (#117) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(iOS): correct route status indicator for dynamic (DNS) routes The status indicator stayed yellow forever for dynamic routes because the previous logic searched for the literal "invalid Prefix" sentinel inside domain strings, which never matched. Match the Android client's logic: a dynamic route is "connected" (green) when any resolved IP for one of its domains appears in a Connected peer's route list. Compare addresses with the CIDR mask stripped, since peer routes carry /32 suffixes while resolved IPs do not. Switch the bridge DTO from a comma-joined ResolvedIPs string to a structured [String] list (mirrors Android's ResolvedIPs collection in client/android/network_domains.go), so consumers no longer depend on the Go-side string formatting. Bumps netbird-core submodule. * debug(iOS): log status indicator decisions in RouteCard Adds verbose NSLog output explaining why each route card resolves to gray, yellow, or green. To be reverted once the dynamic-route status indicator regression is diagnosed. * debug(iOS): route status indicator decisions to AppLogger Replace NSLog with AppLogger.shared.log so traces land in the shared swift-log.log file. Deduplicate per-route decisions so SwiftUI re-renders don't flood the rotating log. * fix(iOS): drop "invalid Prefix" sentinel, align with Android route logic The bridge now exposes Domains.SafeString() as the Network value for dynamic (DNS) routes, matching the Android client. That string also appears in peer.routes (it's what dynamic.Route.String() returns), so a single peer.routes.contains(route.network) check works for both static and dynamic routes — no sentinel branching needed. Replace "invalid Prefix" checks with route.domains presence checks in the status indicator, route display text, and tooltip detail view, on both iOS and tvOS. Bumps netbird-core submodule. * chore(iOS): remove RouteCard status-indicator debug logging Diagnosis is complete; the verbose AppLogger output and dedup cache in statusIndicatorColor are no longer needed. * chore(iOS): point netbird-core submodule at merged upstream commit The bridge change (structured ResolvedIPs collection + dynamic-route Network exposure) has landed on the netbird-core main branch as f23aaa9ae. Replace the local feature-branch SHA with the merged one. --- .../App/Views/Components/RouteCard.swift | 32 ++++++++++++------- .../Source/App/Views/TV/TVNetworksView.swift | 6 ++-- .../PacketTunnelProvider.swift | 6 +++- NetbirdKit/RoutesSelectionDetails.swift | 4 +-- .../PacketTunnelProvider.swift | 6 +++- netbird-core | 2 +- 6 files changed, 36 insertions(+), 20 deletions(-) diff --git a/NetBird/Source/App/Views/Components/RouteCard.swift b/NetBird/Source/App/Views/Components/RouteCard.swift index 12438e5..1066155 100644 --- a/NetBird/Source/App/Views/Components/RouteCard.swift +++ b/NetBird/Source/App/Views/Components/RouteCard.swift @@ -98,20 +98,30 @@ struct RouteCard: View { } private var statusIndicatorColor: Color { - if route.selected && peerViewModel.peerInfo.contains(where: { info in - info.connStatus == "Connected" && (info.routes.contains(route.network ?? "") || route.domains?.contains(where: { $0.domain.contains(route.network ?? "") }) == true) - }) { + guard route.selected else { return Color.gray.opacity(0.5) } + + let connectedPeerRoutes = peerViewModel.peerInfo + .filter { $0.connStatus == "Connected" } + .flatMap { $0.routes } + + if let network = route.network, connectedPeerRoutes.contains(network) { return Color.green } - return route.selected ? Color.yellow : Color.gray.opacity(0.5) + + let resolvedIPs = (route.domains ?? []).flatMap { $0.resolvedIPs } + if resolvedIPs.contains(where: connectedPeerRoutes.contains) { + return Color.green + } + + return Color.yellow } private var routeDisplayText: String { - if route.network == "invalid Prefix" { - if let domains = route.domains, domains.count > 2 { + if let domains = route.domains, !domains.isEmpty { + if domains.count > 2 { return "\(domains.count) Domains" } - return route.domains?.map { $0.domain }.joined(separator: ", ") ?? "" + return domains.map { $0.domain }.joined(separator: ", ") } return route.network ?? "Unknown" } @@ -159,11 +169,9 @@ struct RouteTooltipView: View { @ViewBuilder func detailInfo() -> some View { Group { - if route.network == "invalid Prefix" { - if let domains = route.domains { - ForEach(domains, id: \.self) { domain in - detailRow(label: domain.domain, value: domain.resolvedips ?? "") - } + if let domains = route.domains, !domains.isEmpty { + ForEach(domains, id: \.self) { domain in + detailRow(label: domain.domain, value: domain.resolvedIPs.joined(separator: ", ")) } } else { detailRow(label: "Network", value: route.network ?? "") diff --git a/NetBird/Source/App/Views/TV/TVNetworksView.swift b/NetBird/Source/App/Views/TV/TVNetworksView.swift index 78f87bf..f23b947 100644 --- a/NetBird/Source/App/Views/TV/TVNetworksView.swift +++ b/NetBird/Source/App/Views/TV/TVNetworksView.swift @@ -177,11 +177,11 @@ struct TVNetworkCard: View { } private var routeDisplayText: String { - if route.network == "invalid Prefix" { - if let domains = route.domains, domains.count > 2 { + if let domains = route.domains, !domains.isEmpty { + if domains.count > 2 { return "\(domains.count) Domains" } - return route.domains?.map { $0.domain }.joined(separator: ", ") ?? "" + return domains.map { $0.domain }.joined(separator: ", ") } return route.network ?? "" } diff --git a/NetBirdTVNetworkExtension/PacketTunnelProvider.swift b/NetBirdTVNetworkExtension/PacketTunnelProvider.swift index 929bc7d..2844fea 100644 --- a/NetBirdTVNetworkExtension/PacketTunnelProvider.swift +++ b/NetBirdTVNetworkExtension/PacketTunnelProvider.swift @@ -613,7 +613,11 @@ class PacketTunnelProvider: NEPacketTunnelProvider { let domains = (0..<(route.domains?.size() ?? 0)).compactMap { domainIndex -> DomainDetails? in guard let domain = route.domains?.get(domainIndex) else { return nil } - return DomainDetails(domain: domain.domain, resolvedips: domain.resolvedIPs) + let resolvedIPsRef = domain.getResolvedIPs() + let resolvedIPs: [String] = (0..<(resolvedIPsRef?.size() ?? 0)).map { ipIndex in + resolvedIPsRef?.get(ipIndex) ?? "" + }.filter { !$0.isEmpty } + return DomainDetails(domain: domain.domain, resolvedIPs: resolvedIPs) } return RoutesSelectionInfo( diff --git a/NetbirdKit/RoutesSelectionDetails.swift b/NetbirdKit/RoutesSelectionDetails.swift index 142aeea..62e80e2 100644 --- a/NetbirdKit/RoutesSelectionDetails.swift +++ b/NetbirdKit/RoutesSelectionDetails.swift @@ -59,12 +59,12 @@ extension RoutesSelectionInfo: Equatable { struct DomainDetails: Codable, Hashable { let domain: String - let resolvedips: String? + let resolvedIPs: [String] } extension DomainDetails: Equatable { static func == (lhs: DomainDetails, rhs: DomainDetails) -> Bool { return lhs.domain == rhs.domain && - lhs.resolvedips == rhs.resolvedips + lhs.resolvedIPs == rhs.resolvedIPs } } diff --git a/NetbirdNetworkExtension/PacketTunnelProvider.swift b/NetbirdNetworkExtension/PacketTunnelProvider.swift index 380ee20..247c622 100644 --- a/NetbirdNetworkExtension/PacketTunnelProvider.swift +++ b/NetbirdNetworkExtension/PacketTunnelProvider.swift @@ -474,7 +474,11 @@ class PacketTunnelProvider: NEPacketTunnelProvider { let domains = (0..<(route.domains?.size() ?? 0)).compactMap { domainIndex -> DomainDetails? in guard let domain = route.domains?.get(domainIndex) else { return nil } - return DomainDetails(domain: domain.domain, resolvedips: domain.resolvedIPs) + let resolvedIPsRef = domain.getResolvedIPs() + let resolvedIPs: [String] = (0..<(resolvedIPsRef?.size() ?? 0)).map { ipIndex in + resolvedIPsRef?.get(ipIndex) ?? "" + }.filter { !$0.isEmpty } + return DomainDetails(domain: domain.domain, resolvedIPs: resolvedIPs) } return RoutesSelectionInfo( diff --git a/netbird-core b/netbird-core index b19b746..f23aaa9 160000 --- a/netbird-core +++ b/netbird-core @@ -1 +1 @@ -Subproject commit b19b7464eac5c58bb6a6780a033398a27f3d772f +Subproject commit f23aaa9ae7097c3f47e50efe0f418f40a90fd4d7