Skip to content

Fix route status indicator for dynamic (DNS) routes#117

Merged
pappz merged 7 commits intomainfrom
fix/route-status-indicator-dynamic-domains
May 7, 2026
Merged

Fix route status indicator for dynamic (DNS) routes#117
pappz merged 7 commits intomainfrom
fix/route-status-indicator-dynamic-domains

Conversation

@pappz
Copy link
Copy Markdown
Contributor

@pappz pappz commented May 6, 2026

Description

The route status indicator stayed yellow forever for dynamic (DNS) routes. The Swift code branched on a route.network == "invalid Prefix" sentinel and tried to substring-match domains against that sentinel, which never matched.

Realign the iOS pipeline with the Android client so a single check works for both static and dynamic routes:

Bridge (netbird-core)

  • Expose Domains.SafeString() as the Network value for dynamic routes (matching client/android/client.go). The same string is what dynamic.Route.String() returns and is therefore what ends up in peer.routes, so peer.routes.contains(route.network) now matches directly.
  • Group resolved IPs by ParentDomain (mirroring client/server/network.go). The resolvedDomains map is keyed by the resolved domain, not the configured pattern; the previous lookup resolvedDomains[d] could never hit for wildcard patterns.
  • Switch the bridge DTO from a comma-joined ResolvedIPs string to a structured ResolvedIPs collection (Add/Get/Size), mirroring client/android/network_domains.go. Swift consumers no longer depend on Go-side string formatting.

Swift app

  • RouteCard.statusIndicatorColor becomes a single, Android-equivalent check: peer.routes.contains(route.network), with a resolved-IP fallback for IP-only static-IP routes.
  • Drop the "invalid Prefix" sentinel from RouteCard.swift and TVNetworksView.swift — branch on route.domains.isEmpty instead.
  • RoutesSelectionDetails.DomainDetails carries resolvedIPs: [String] instead of an optional CSV string.

Bumps netbird-core submodule (corresponding upstream PR contains the bridge changes).

Summary by CodeRabbit

  • Bug Fixes
    • Improved route status colors: selected routes show green when matched or resolved to connected peers, non-selected routes show gray, others show yellow.
    • Route display text now prefers domain names (or a count) and falls back to network when none exist.
    • Tooltips list each domain with their resolved IPs and omit a separate network row when domains are present.
  • Chores
    • Updated core dependencies.

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.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 6, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c87079fd-a3fb-4507-a9a1-c518be501ede

📥 Commits

Reviewing files that changed from the base of the PR and between 1ef41a8 and 090d5aa.

📒 Files selected for processing (1)
  • netbird-core
✅ Files skipped from review due to trivial changes (1)
  • netbird-core

📝 Walkthrough

Walkthrough

This PR replaces DomainDetails.resolvedips: String? with resolvedIPs: [String], updates code paths to compute and pass resolved IP arrays from domain resolution, adjusts UI to compute route status using selection, peer connectivity and resolved IP matching, and bumps the netbird-core submodule pointer.

Changes

Resolved IP data-shape, resolution plumbing, and UI status logic

Layer / File(s) Summary
Data Shape
NetbirdKit/RoutesSelectionDetails.swift
Removed DomainDetails.resolvedips: String?; added DomainDetails.resolvedIPs: [String]; Equatable now compares resolvedIPs.
Resolver / Data Construction
NetBirdTVNetworkExtension/PacketTunnelProvider.swift, NetbirdNetworkExtension/PacketTunnelProvider.swift
getSelectRoutes now calls domain.getResolvedIPs(), builds a filtered [String] resolvedIPs array and constructs DomainDetails(domain: ..., resolvedIPs: resolvedIPs) (argument label renamed).
Core UI / Presentation Logic
NetBird/Source/App/Views/Components/RouteCard.swift, NetBird/Source/App/Views/TV/TVNetworksView.swift
statusIndicatorColor now requires route.selected and checks connected peer addresses against resolvedIPs to choose green/yellow/gray. routeDisplayText simplified to prefer domains (count or joined names), otherwise uses network. Tooltip/detail rows list each domain with resolvedIPs.joined(", ").
Submodule Pointer
netbird-core submodule
Submodule pointer updated to a new commit hash.

Sequence Diagram(s)

sequenceDiagram
    participant UI as RouteCard / TVNetworksView
    participant App as App Logic
    participant NE as PacketTunnelProvider (NetworkExtension)
    participant Resolver as Domain Resolver
    participant Peers as PeerInfo Store

    UI->>App: request route display & status
    App->>NE: request selected routes with domain details
    NE->>Resolver: domain.getResolvedIPs()
    Resolver-->>NE: resolved IP list
    NE-->>App: DomainDetails(domain, resolvedIPs)
    App->>Peers: query connected peers / addresses
    Peers-->>App: connected peer addresses
    App->>UI: provide routeDisplayText and statusIndicatorColor (match resolvedIPs vs connected addresses)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • netbirdio/ios-client#82: Related UI change — modifies RouteCard.statusIndicatorColor behavior regarding route.selected.

Suggested reviewers

  • mlsmaycon
  • lixmal

Poem

🐰 A rabbit scans the network's flow,

Domains unfold where IPs now glow.
Resolved lists hop from resolver root,
Peers and cards find matching suit.
Hop on, tunnels — code is spry and sprout!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: fixing the route status indicator for dynamic DNS routes by correcting the status detection logic.
Description check ✅ Passed The PR description is comprehensive and complete, detailing the root cause, changes across bridge and Swift components, and the rationale for alignment with Android.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/route-status-indicator-dynamic-domains

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
NetbirdKit/RoutesSelectionDetails.swift (1)

60-63: ⚡ Quick win

Keep DomainDetails decoding backward-compatible.

Lines 60-63 change the IPC payload from a legacy resolvedips: String? field to resolvedIPs: [String]. That is a breaking wire-format change, so a stale extension process or any older cached plist will fail to decode the whole routes payload. Please add a custom init(from:) that accepts both shapes and defaults missing data to [].

Proposed compatibility shim
 struct DomainDetails: Codable, Hashable {
     let domain: String
     let resolvedIPs: [String]
+
+    enum CodingKeys: String, CodingKey {
+        case domain
+        case resolvedIPs
+        case legacyResolvedIPs = "resolvedips"
+    }
+
+    init(domain: String, resolvedIPs: [String]) {
+        self.domain = domain
+        self.resolvedIPs = resolvedIPs
+    }
+
+    init(from decoder: Decoder) throws {
+        let container = try decoder.container(keyedBy: CodingKeys.self)
+        domain = try container.decode(String.self, forKey: .domain)
+
+        if let resolvedIPs = try container.decodeIfPresent([String].self, forKey: .resolvedIPs) {
+            self.resolvedIPs = resolvedIPs
+        } else if let legacyResolvedIPs = try container.decodeIfPresent(String.self, forKey: .legacyResolvedIPs) {
+            self.resolvedIPs = legacyResolvedIPs
+                .split(separator: ",")
+                .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
+                .filter { !$0.isEmpty }
+        } else {
+            self.resolvedIPs = []
+        }
+    }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@NetbirdKit/RoutesSelectionDetails.swift` around lines 60 - 63, DomainDetails'
Codable decoding must accept both the new resolvedIPs: [String] shape and the
legacy resolvedips: String? to remain backward-compatible; implement a custom
init(from decoder: Decoder) in the DomainDetails struct that attempts to decode
resolvedIPs as [String], falls back to decoding resolvedips as a single optional
String (splitting or wrapping it into an array if present), and defaults
resolvedIPs to [] when neither field exists or decoding fails, ensuring the
decoded properties (domain and resolvedIPs) are populated accordingly.
NetBird/Source/App/Views/Components/RouteCard.swift (1)

107-108: ⚡ Quick win

Stop keying dynamic-route behavior off "invalid Prefix".

Lines 107-108 and Line 180 still couple the UI to a Go-side sentinel string. This PR removed the old string-format dependency around resolved IPs, but this one can still regress the status/detail logic if core changes that placeholder again. Prefer deriving isDynamicRoute from domains?.isEmpty == false or carrying an explicit flag in the DTO.

Possible cleanup
+    private var isDynamicRoute: Bool {
+        !(route.domains?.isEmpty ?? true)
+    }
+
     private var statusIndicatorColor: Color {
         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, network != "invalid Prefix",
+        if !isDynamicRoute, let network = route.network,
            connectedPeerRoutes.contains(network) {
             return Color.green
         }
@@
     private var routeDisplayText: String {
-        if route.network == "invalid Prefix" {
+        if isDynamicRoute {
             if let domains = route.domains, domains.count > 2 {
                 return "\(domains.count) Domains"
             }
             return route.domains?.map { $0.domain }.joined(separator: ", ") ?? ""
         }
         return route.network ?? "Unknown"
     }
@@
     func detailInfo() -> some View {
         Group {
-            if route.network == "invalid Prefix" {
+            if isDynamicRoute {
                 if let domains = route.domains {
                     ForEach(domains, id: \.self) { domain in
                         detailRow(label: domain.domain, value: domain.resolvedIPs.joined(separator: ", "))
                     }
                 }

Also applies to: 180-180

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@NetBird/Source/App/Views/Components/RouteCard.swift` around lines 107 - 108,
The view currently treats a route as dynamic by checking route.network !=
"invalid Prefix", which hard-codes a Go sentinel string; instead change the
logic in RouteCard (where route.network and connectedPeerRoutes are used) to
derive dynamic routes from route.domains?.isEmpty == false (or an explicit DTO
flag if available) and update the same check at the other occurrence (the second
use around line 180) to use the new condition; ensure any UI branches that
relied on the "invalid Prefix" string now use the domains emptiness check or DTO
flag so the UI is no longer coupled to that sentinel value.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@NetBird/Source/App/Views/Components/RouteCard.swift`:
- Around line 107-108: The view currently treats a route as dynamic by checking
route.network != "invalid Prefix", which hard-codes a Go sentinel string;
instead change the logic in RouteCard (where route.network and
connectedPeerRoutes are used) to derive dynamic routes from
route.domains?.isEmpty == false (or an explicit DTO flag if available) and
update the same check at the other occurrence (the second use around line 180)
to use the new condition; ensure any UI branches that relied on the "invalid
Prefix" string now use the domains emptiness check or DTO flag so the UI is no
longer coupled to that sentinel value.

In `@NetbirdKit/RoutesSelectionDetails.swift`:
- Around line 60-63: DomainDetails' Codable decoding must accept both the new
resolvedIPs: [String] shape and the legacy resolvedips: String? to remain
backward-compatible; implement a custom init(from decoder: Decoder) in the
DomainDetails struct that attempts to decode resolvedIPs as [String], falls back
to decoding resolvedips as a single optional String (splitting or wrapping it
into an array if present), and defaults resolvedIPs to [] when neither field
exists or decoding fails, ensuring the decoded properties (domain and
resolvedIPs) are populated accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: de21a625-540f-464d-a3a2-df53cb34d6a6

📥 Commits

Reviewing files that changed from the base of the PR and between c76475b and 58583e2.

📒 Files selected for processing (5)
  • NetBird/Source/App/Views/Components/RouteCard.swift
  • NetBirdTVNetworkExtension/PacketTunnelProvider.swift
  • NetbirdKit/RoutesSelectionDetails.swift
  • NetbirdNetworkExtension/PacketTunnelProvider.swift
  • netbird-core

lixmal
lixmal previously approved these changes May 6, 2026
@pappz
Copy link
Copy Markdown
Contributor Author

pappz commented May 6, 2026

/testflight

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 6, 2026

TestFlight builds uploaded 0.2.1 (5) for 58583e2 — iOS + tvOS

View workflow run

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.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@NetBird/Source/App/Views/Components/RouteCard.swift`:
- Around line 101-160: The code emits many NSLog calls with sensitive network
metadata (see the tag variable, peerViewModel.peerInfo, route.network,
route.domains, resolvedIPs, connectedPeerRoutes and all NSLog(...) lines);
remove these or wrap all NSLog calls in a compile-time guard (e.g. `#if` DEBUG ...
`#endif`) so they are excluded from release/TestFlight builds, or replace them
with a single non-sensitive debug-only log; ensure you update every NSLog
invocation in this RouteCard computed view to be behind that guard.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 681c6231-1b52-43b1-96a2-ea722649d61a

📥 Commits

Reviewing files that changed from the base of the PR and between 58583e2 and 22f1825.

📒 Files selected for processing (1)
  • NetBird/Source/App/Views/Components/RouteCard.swift

Comment thread NetBird/Source/App/Views/Components/RouteCard.swift Outdated
@pappz
Copy link
Copy Markdown
Contributor Author

pappz commented May 6, 2026

/testflight

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 6, 2026

TestFlight builds uploaded 0.2.1 (7) for 6c3e0d7 — iOS + tvOS

View workflow run

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.
@pappz
Copy link
Copy Markdown
Contributor Author

pappz commented May 6, 2026

/testflight

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 6, 2026

TestFlight builds uploaded 0.2.1 (8) for d6832c9 — iOS + tvOS

View workflow run

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.
@pappz
Copy link
Copy Markdown
Contributor Author

pappz commented May 6, 2026

/testflight

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 6, 2026

TestFlight builds uploaded 0.2.1 (9) for d00d68f — iOS + tvOS

View workflow run

Diagnosis is complete; the verbose AppLogger output and dedup cache
in statusIndicatorColor are no longer needed.
@pappz pappz requested a review from lixmal May 6, 2026 13:44
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
NetBird/Source/App/Views/TV/TVNetworksView.swift (1)

179-187: ⚡ Quick win

routeDisplayText is duplicated with RouteCard with a silent empty-string fallback.

The logic here is identical to RouteCard.routeDisplayText except the no-domain, no-network fallback is "" (renders an invisible Text) rather than "Unknown". Centralising this on RoutesSelectionInfo (or a shared extension) would eliminate the divergence and make a future threshold change a single-site edit.

♻️ Suggested extraction onto RoutesSelectionInfo
// In RoutesSelectionInfo (or an extension file):
var displayText: String {
    if let domains = domains, !domains.isEmpty {
        return domains.count > 2
            ? "\(domains.count) Domains"
            : domains.map { $0.domain }.joined(separator: ", ")
    }
    return network ?? "Unknown"
}

Then both call sites become a single route.displayText.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@NetBird/Source/App/Views/TV/TVNetworksView.swift` around lines 179 - 187,
Duplicate route display logic exists in TVNetworksView.routeDisplayText and
RouteCard.routeDisplayText; extract it into a single computed property on
RoutesSelectionInfo (or an extension) named something like displayText that
implements the domains/count > 2 branch, joins domain names when <= 2, and
returns network ?? "Unknown" as the fallback. Replace both
TVNetworksView.routeDisplayText and RouteCard.routeDisplayText usages with
route.displayText so they share the same implementation and threshold; ensure
you remove the silent-empty-string fallback in TVNetworksView and use the
centralized property instead.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@NetBird/Source/App/Views/TV/TVNetworksView.swift`:
- Around line 179-187: Duplicate route display logic exists in
TVNetworksView.routeDisplayText and RouteCard.routeDisplayText; extract it into
a single computed property on RoutesSelectionInfo (or an extension) named
something like displayText that implements the domains/count > 2 branch, joins
domain names when <= 2, and returns network ?? "Unknown" as the fallback.
Replace both TVNetworksView.routeDisplayText and RouteCard.routeDisplayText
usages with route.displayText so they share the same implementation and
threshold; ensure you remove the silent-empty-string fallback in TVNetworksView
and use the centralized property instead.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: aa237f7d-3a21-4cc3-8580-1cc6df40187f

📥 Commits

Reviewing files that changed from the base of the PR and between 22f1825 and 1ef41a8.

📒 Files selected for processing (3)
  • NetBird/Source/App/Views/Components/RouteCard.swift
  • NetBird/Source/App/Views/TV/TVNetworksView.swift
  • netbird-core

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.
@pappz pappz merged commit 3f67c8b into main May 7, 2026
11 checks passed
@pappz pappz deleted the fix/route-status-indicator-dynamic-domains branch May 7, 2026 11:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants