mirror of
https://github.com/netbirdio/ios-client.git
synced 2026-09-23 09:28:33 -07:00
main
51
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ecf3b2a82f |
Add a Troubleshoot toggle for remote debug bundle jobs (#216)
* Add a Troubleshoot toggle for remote debug bundle jobs The client refuses management-requested remote jobs unless RemoteJobsAllowed is enabled, and the iOS and tvOS apps had no way to turn it on. Expose the flag through ConfigurationProvider, add an "Allow remote debug bundles" toggle to the iOS Troubleshoot screen and a Troubleshooting section in the tvOS settings, and bump netbird-core to the commit that adds the SDK preference accessors. * Update submodule * Route the iOS remote jobs toggle through the view model setter The Toggle wrote the published property before onChange invoked setRemoteJobsAllowed, so on a failed commit the rollback restored the new value instead of the previous one. Use a Binding whose setter calls the view model directly, matching the tvOS settings view. * Lock the remote debug bundle toggle when MDM manages it (#224) * Lock the remote debug bundle toggle when MDM manages it Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Decode the remote jobs snapshot key as allowRemoteJobs The Go snapshot reports this field under the policy key name, allowRemoteJobs, not remoteJobsAllowed. The synthesized coding key used the property name, so decodeIfPresent never found the key and the flag stayed false — the toggle looked editable under an MDM policy that manages it. Map the coding key explicitly and keep the property name, which the views already reference. Bumps netbird-core to pick up the matching Go-side fix. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Bump submodule * Use allowRemoteJobs key in MDM restriction test fixtures Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * Enforce the remote jobs policy before the next poll setRemoteJobsAllowed validated against the cached MDM snapshot, which is only refreshed on onAppear or a 30s stale poll. The OS writes managed configuration from another process, so the in-process change notification never fires for it and a policy pushed mid-session went unenforced until the next poll. Refresh the snapshot before validating. The guard also checked only mdm.remoteJobsAllowed, while both toggles lock on remoteJobsAllowed || disableUpdateSettings, so the backstop could disagree with the control the user sees. Extract remoteJobsForbiddenByPolicy to mirror the lock. Route the rejection through commitSettings() so an MDM-refused commit raises the standard alert instead of a silent print. This matters most on tvOS, where commit() always reports success and the pre-commit guard is the only backstop. * Restore the persisted remote jobs setting after the test testUnmanagedDeviceStillAcceptsTheChange writes through ConfigurationProvider, which persists into the Go preferences store. tearDown() only restored the MDM dictionary, so an enabled remoteJobsAllowed could leak into later tests or a reused test-host launch. Capture the original value and restore it via defer, which runs while the device is still unmanaged so the setter is not rejected. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
6116ea921c |
Show exit nodes right after connect without visiting another tab (#232)
* Show exit nodes right after connect without visiting another tab The exit node selector stayed on "No exit nodes available" after every connect until the user opened the Peers or Resources tab and came back. The extension reports the tunnel connected from onConnected, which the core fires once management and signal are up - before the engine is reachable and a few hundred milliseconds before the first network map arrives. The app reads the routes on that .connected transition, the core answers "not connected", and the extension swallowed that error into an empty route list. The app takes any answer from the core as the truth about the network map, so it cached zero routes. Nothing re-read them afterwards: the status poll only fetches status, and route reads happen on .connected and on a view's onAppear, which is why switching tabs "fixed" it. The extension log shows the same pair on every connect, a few milliseconds apart: onConnected: state=connected Error retrieving or encoding route selection details: not connected Two changes. getSelectRoutes now answers a core error with no payload, which the adapter already treats as a failed read that leaves the cache alone - an error is not zero routes. And the extension now tells the app when the network map moves: the core's onPeersListChanged fires at the end of every network map update, after the route manager has applied the routes, and on every route/peer state change, so the previously empty callback posts a Darwin notification (the same mechanism the app uses to announce MDM policy changes to the extension, in the other direction). Android's HomeFragment relies on the same event for the same reason. RoutesViewModel observes that notification and re-reads the routes, coalesced to one read per 300 ms window: the core announces a change per peer and per route, so a connect with dozens of peers produces dozens of notifications within seconds, and each read is an IPC round-trip that serialises the whole route list. One pending work item absorbs every notification until it fires; a change announced after the fire schedules the next, so nothing is lost. Optimistic selection writes and clearRoutes() retire in-flight and pending reads, so a read dispatched before a tap can no longer answer after it with the pre-tap selection; the mutation's own reconcile read is the one allowed to answer. * Drop network map notifications overtaken by a clear or a selection write scheduleNetworkMapRefresh hops to the main queue before it looks at the pending refresh, and that hop is a window. A notification still in transit when clearRoutes() or beginSelectionMutation() ran on main found the pending slot they had just emptied and scheduled a fresh read after the very invalidation that should have dropped it. After clearRoutes() that read is not harmless: the optimistic disconnect and the profile switch clear the routes while the extension is still winding down and still announcing peer changes, so the read can succeed and repopulate a list nothing clears again - the selector enabled over nodes the core can no longer apply. Capture a lock-guarded epoch when the notification arrives, on whatever thread the Darwin notify center delivers on, advance it in clearRoutes() and beginSelectionMutation(), and re-check it once the request reaches main. A notification that was valid when it arrived and is still valid on main schedules exactly as before. |
||
|
|
46ba1cb106 |
Fix exit node (#226)
* Fix exit node * Bound the route IPC round-trip so its completion always fires sendProviderMessage only calls its reply handler if the extension answers. An extension that never does - wedged in a synchronous Go call, gone, or returning out of handleAppMessage before replying - stranded the completion that getRoutes/selectRoutes/deselectRoutes promise on every path. That also hung the DispatchGroup RoutesViewModel.selectRoute balances around the sibling deselects, so the pending select was never sent and the optimistic exit node selection was never reverted. Bound the wait with a DispatchWorkItem and deliver exactly one outcome through an NSLock-guarded latch, cancelling the timeout when a reply or a send error completes the request - the same pattern fetchData already uses for the status round-trip. Adds RouteRequestError.timedOut. Also promotes sendSelectAndReconcile's comment to a doc comment. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Keep the On Demand preference when a re-arm is refused rearmOnDemandIfNeeded wrote connectOnDemand = false on .failed, guarded only by `guard self.connectOnDemand`. But true is exactly the value a newer, successful re-arm - or the user's own Settings toggle - leaves behind, so that guard cannot tell a stale failure from a fresh success. Nothing pinned the completion to the request that started it. The window is wide: applyOnDemandState only reports .failed after recoverFromFailedOnDemandWrite has run a full loadFromPreferences round-trip. Two overlapping re-arms are enough to hit it with no user involved - applyStatusSideEffects(.connected) runs from both applyExtensionStatus and startActivation, and the very reload that clears configurationStale is what lets the second attempt succeed. The late failure from the first then clobbered rules that were genuinely armed. The write was also wrong in kind. Unlike setConnectOnDemand it makes no optimistic write of its own, so there is no previous value to roll back to; and false closes the guard, so a single transient NEVPNError.configurationStale would discard the user's setting for good instead of letting the next connect retry. "Preference on, rules not armed yet" is a state the app already creates on purpose - see checkLoginRequiredFlag(). Log the refusal and leave the intent standing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
072a881a3e |
Deliver the MDM policy to the network extension through the App Group (#227)
* Gate tvOS writes and bound the MDM restart retry tvOS never reached CheckMDMConflicts: commit() there returns true without consulting anything, and settings go straight to the config JSON, so a managed field stayed writable while iOS rejected it. The gate belongs in the setters — by commit() time the write has already happened — and it phrases the refusal the way Go does, so both platforms say the same thing. The refusal text was written in three places while the key list Go puts in the error was thrown away. One constant now, and the wrapped conflicts are parsed out so the user learns which setting was refused. pendingMDMRestart was written both synchronously from stopObservingMDMConfigChanges and from inside a monitorQueue block; it now sits behind tearDownLock with the rest of the lifecycle state. The deferred retry rescheduled itself indefinitely. Cap it at 15 attempts — thirty seconds, the restart pipeline's own timeout — then log and stop, since waiting longer only hides a wedged pipeline. * Use one policy snapshot per tvOS write and drop the duplicate start latch * eliver the MDM policy to the network extension through the App Group * Update ConfigurationProvider.swift * Derive the MDM retry budget from the watchdogs it waits on |
||
|
|
e8dbf9c2fa |
Fire the adapter's stop completion only after the Go run loop has exited (#223)
* Fire the adapter's stop completion only after the Go run loop has exited The restart path used the connection listener's onDisconnected callback as the "stop completed" signal and started the next engine from inside it. The Go notifier delivers that callback synchronously from the outgoing Run(), before finishRun() releases the run slot, so the immediate Run() could hit the already-running guard in startRun(). Run() returns that error without logging it, the adapter replaced it with a generic message, and the tunnel stayed installed with no engine behind it. A 0.3.4 debug bundle shows 15 such failed restarts in 13 days; six ended in a device-wide blackhole until the user toggled the VPN. - NetBirdAdapter.stop runs the blocking client.stop() on a dedicated serial queue and invokes the completion only after it returns; Client.Stop() already waits for the run loop to exit. The 15 s fallback and the listener-driven notifyStopCompleted are gone, and consecutive stops queue up instead of firing each other's handler. - Without a completion the call stays synchronous, so the tvOS provider is unchanged. waitForExit: false uses StopWithoutWait, which stopTunnel now passes because iOS gives it only a few seconds. - The start catch path no longer calls stop(): Stop() cancels whichever run is registered, so a late failure of a superseded run could kill the healthy one. The real Go error is forwarded instead of a fixed string. - startTunnel resets adapter.isRestarting alongside isRestartInProgress. * Keep the tvOS provider off the blocking stop path restartClient stopped the client synchronously on the path monitor's queue, so path updates and their debounce could not be delivered for the duration of the Go teardown. Use the completion-based stop, which waits for the run loop to exit on the adapter's own queue, and start the client from there. stopTunnel passes waitForExit: false, as on iOS: the system gives it only a few seconds and it already delays its completion by two more. The restart error is logged as public so the forwarded Go error text is readable in Console. |
||
|
|
8fa8d34ff3 |
Mdm integration (#206)
* MDM policy fetcher * Bump head of netbird mdm_integration branch * Bump netbird-core to mdm_integration head * Enforce MDM policy in the app process and across the UI The extension already registered the policy fetcher on its Client, but the main app ran with no policy at all: login and settings read an empty policy and silently ignored MDM. Register the fetcher on everything that reads or writes config — Preferences, the Go-backed ProfileManager, and all four NewAuth call sites — and drive the screens from the enforcement snapshot the Go layer renders. MDMPolicyFetcher.swift was in the repository but in no target, so the file the extension referenced was never compiled. Add it, and the new MDMRestrictions decoder, to the four targets that need them. Decode with decodeIfPresent throughout rather than the synthesized conformance, so a Go layer that adds or drops a key cannot break the parse, and let any read failure degrade to "nothing managed": a broken policy must not lock a user out of their own settings. UI convention: a gated feature (features.*) hides its section outright, while a managed setting stays visible and locked with "Managed by your organization". A control that silently vanishes reads as a bug; one that is visibly locked explains itself. Replace the Swift-side change detection with hasMDMPolicyChanged(), so the per-key diff and the dedup that the shared UserDefaults channel requires both live in Go. Surface a commit() rejection as a managed-setting message instead of a raw error. Report an applied policy through the same flag-plus- notification delivery already used for the login-required signal, since an extension cannot raise a toast itself. Preferences.getPreSharedKey() is gone from the SDK — the key no longer crosses the bridge — so ConfigurationProvider exposes a write-only setter alongside hasPreSharedKey(), and the screens show whether a key is configured rather than its value. Tests cover the decoder (tri-state allowServerSSH, missing, unknown and malformed input) and the Swift↔Go round trip against a pushed managed configuration. They skip with an explanation when a scheme launch argument pins a policy, since the argument domain outranks every other and would otherwise make them report a false result. * Review findings, all verified against the code before fixing * Guard every On Demand re-arm with the MDM policy * Skip the On Demand disconnect prompt while the policy disarms the rules * Serialize the MDM restart and report the policy only once it is applied * Bound how long a pushed policy can go unnoticed in the foreground * Close three paths that mutate state the policy forbids * Abandon a restart that a teardown overtakes * Make the teardown latch observable from every queue * Update PacketTunnelProvider.swift * Update PacketTunnelProvider.swift * Bump the core to the MDM branch head and follow its contract changes * Bump netbird core to the main --------- Co-authored-by: riccardom <riccardomanfrin@gmail.com> Co-authored-by: evgeniyChepelev <68751844+evgeniyChepelev@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
59839d0c7e |
fix: harden iOS handoff recovery and resource lifecycle (#212)
* fix: tighten iOS resource and reconnect lifecycle Use the tunnel provider's long-lived path state during disconnect callbacks, keep restart ownership in the restart transaction, break the foreground polling retain cycle, deliver status updates outside locks on the main queue, clean up observers, and batch log synchronization to reduce I/O wakeups. * fix: harden iOS network handoff recovery Serialize restart transactions, queue path changes that arrive mid-restart, reject stale callbacks, avoid starting on an unavailable path, and retry non-auth startup failures with bounded exponential backoff. Also reduce foreground polling frequency and skip malformed optional Firebase configuration instead of aborting at launch. * fix: address reconnect review feedback * fix: resolve signed flavor tunnel provider * fix: address follow-up review findings * fix: show disconnect intent immediately * docs: document VPN lifecycle changes * fix: clear stale disconnect intent after status refresh |
||
|
|
7b382045b8 |
Stop the tunnel from restarting itself during a normal connect (#220)
* Stop the tunnel from restarting itself during a normal connect The extension monitored an unfiltered network path, which also covers the utun interface the provider itself creates. A single connect applies the tunnel settings several times — the engine pushes the interface address, the routes and the DNS config separately — and each apply flapped that path. handleNetworkChange read every flap as an outage followed by a recovery and answered it with a full client restart, which applied the settings again and flapped the path once more: a loop that kept the tunnel cycling between connected and disconnected until the timing happened to miss the detection window. The main app's 3s status poll surfaced the cycle as an unreliable, drawn-out connect. Break the loop and the conditions that widened it: - Exclude the provider's own tunnel interface from the path monitor, so applying tunnel settings is no longer visible to it. This matches what the Android client already does by requiring NET_CAPABILITY_NOT_VPN. - Keep only one monitor live. startMonitoringNetworkChanges overwrote pathMonitor without cancelling it, so a second startTunnel in the same extension process left both monitors handling every path update. Tear the monitor down on the queue that starts it and delivers its updates, rather than from the main thread, so cancellation cannot race an in-flight update. - Require the path to stay unsatisfied for 2s before declaring an outage. A shorter gap is a blip and must produce neither the UI's network warning nor a restart. - Hold off network-change restarts until the initial connect reports its outcome, so a restart cannot tear down a connection that is still coming up. A bounded guard keeps a start that never reports from blocking restarts for the rest of the tunnel's life. - Skip the On Demand preferences write when the requested state and rules already match. saveToPreferences on a configured manager makes NE emit a transient .disconnecting, and applyExtensionStatus arms On Demand on every transition to .connected, so an unconditional write added a spurious disconnect to every connect. - Report the startTunnel outcome to NE exactly once. The Go notifier re-fires OnConnected on any management/signal reconnect within one run (peer/notifier.go dedupes only consecutive identical states), so the adapter's start callback announced a fresh tunnel start for every mid-session reconnect. * Do not trust the manager's On Demand state after a failed save Skipping the write when the requested state already matched assumed the retained manager reflects the system preferences. A failed saveToPreferences breaks that: the assigned isOnDemandEnabled and onDemandRules stay on the manager while the system still holds the old values, so the next identical request satisfied the equality check and reported .applied without writing anything. closeWithOnDemandDisabled waits for that .applied before dropping the tunnel, and would have disconnected with the rules still in force — which has the system bring the tunnel straight back. Reload the manager after a failed save so the values that were never persisted come off it, and report the failure only once that reload has settled. Reloading is also the documented recovery from NEVPNError.configurationStale, the case where another process wrote the configuration in between. The reload can fail too, leaving the manager untrustworthy, so also latch the failure: while it is set the no-write path is closed and every request writes. The latch clears on the next successful save and is guarded by the lock that already protects the neighbouring requested-state property, since these requests arrive both from the main queue and from the async login path. * Report the start outcome when a stop interrupts the tunnel startup The start outcome is delivered from the connection listener's onConnected, so it is bound to the connection succeeding rather than to the start attempt ending. A stop that lands before the engine ever connects just makes the run loop return — client.run does not throw, and onDisconnected only completes the stop — so nothing reported the outcome and NE was left waiting on startTunnel until it timed out. Complete it from stopTunnel with a cancellation error. The latch makes this safe: a start that already reported is untouched, so an ordinary disconnect from a connected tunnel and the login-required path both stay as they were. Also fix the logging the latch introduced. Its "outcome already reported" line would have been written on every ordinary disconnect and read as a failure. completeTunnelStart now reports whether it delivered, and each call site logs the case that is actually worth reading: a repeated onConnected from an engine reconnect at the start site, and a stop before connect at the stop site. |
||
|
|
6d3407f970 |
Capture Go panics in the main app and report them to Crashlytics (#215)
* Capture Go panics in the main app and report them to Crashlytics A Go panic aborts the process with SIGABRT, and both the Apple crash report and Crashlytics stop at runtime.asmcgocall: the Go stack switch breaks the unwinder, so the panicking Go frames are never shown. The panic message and goroutine dump only go to stderr. The network extension already redirected stderr into netbird.err in the app group container, but the main app did not, so a panic in the Go code the app itself calls (login, preferences, profile management) left no trace anywhere. Move the redirect into a shared GoCrashCapture in NetbirdKit and call it from both processes before any Go SDK call. On launch, after Firebase is configured, the app forwards whatever netbird.err gained since the last report to Crashlytics: the full dump via log() and a non-fatal whose headline is the panic line. The file itself is never truncated, so the debug bundle keeps picking it up unchanged. * Keep netbird.err shared across processes and report backlogs one dump at a time Truncate the file in place when it passes the size cap instead of unlinking it: the other process may hold fd 2 on the same inode, and an unlink would send its later panic output into an orphan nobody reads. Advance the reported offset only past the chunk actually returned. The read is capped at 48 KB, so jumping to the end of the file skipped any second dump that landed behind the first one between two app launches. |
||
|
|
eedd8b6fc1 |
Remove connection state indicator from resource list cards (#214)
* fix: remove connection state indicator from resource list cards * fix: drop stale statusIndicatorColor mention from route status comment |
||
|
|
37de9d9065 |
fix(ios): repeated 2FA prompt during SSO re-login (iOS) (#175)
* fix(ios): present the SSO login browser as a sheet * fix(ios): address review findings in the SSO login flow don't log the loopback redirect URL — its query carries the OAuth authorization code; log only the fact and host - rearm the login-sheet completion flag in onDismiss instead of the content's onAppear, and dismiss-and-cancel defensively if the sheet is ever presented without a valid login URL - tag each login attempt with a token and check it in the 20 s post-dismissal fallback, so a stale timer can't cancel a newer attempt (including the automatic identity-reset retry) - harden the ownership-conflict self-heal: skip the retry when the identity reset itself fails (try? previously hid the error and the retry re-presented the same conflicting key) and gate it on the browser phase having started, so it can't race the IPC fallback - document why navigation-failure handlers act only after the loopback redirect: pre-success failures include benign NSURLErrorCancelled from superseded navigations, and reacting to them would abort healthy logins * fix(ios): run SSO login in the system auth session instead of a webview * fix(ios): reuse the browser session on re-login instead of clearing it * fix(ios): serialize the loopback probe and clarify the callback design * fix(ios): bind the account to the profile instead of the cookie jar * Update submodule and fix conflicts * fix(ios): keep the authorize URL out of the log and profile lookups out of body * Point the submodule back at upstream netbird The fork override was temporary: it carried the login_hint support this branch needs while it was unmerged. That support is upstream now — the account-email layer lives in client/mobile and the submodule already points at an upstream commit — so the override has nothing left to carry. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Name the browser-session key what it is: a profile ID It has held the ID since the move onto the Go profile manager, but the parameter was still called `name`, which is exactly the ambiguity a reviewer asked about: a display name would break under rename, an ID does not. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Let the Go SDK decide the login account The branch rewrote the authorize URL in Swift to append prompt=login whenever it judged the browser session might resolve to the wrong account. The Go SDK already does this, better: pkce_flow.go sets prompt=login (or max_age=0) from the management server's login flag, and skips it entirely for IdPs marked DisablePromptLogin — a flag the Swift rewriting could not see, so it forced a prompt on exactly the providers that break under one. It also could not work as intended. The core sends prompt=login by default, so the Swift path found a prompt already present and bailed out; the account chooser it was supposed to force never fired on a default deployment. Account targeting now comes from the login_hint the SDK sends, which removes the reason for the browser-session bookkeeping too: which profile last held the session was only ever an input to this decision. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Drop the comment left behind by the removed browser-session key Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * CI: build the core from netbird#7193 while it is unmerged This branch needs the iOS login_hint wiring that netbirdio/netbird#7193 adds: without it the SDK sends no hint and a re-login is resolved by whatever the shared browser session holds, which is the bug this branch is fixing. Fetch the PR head from netbirdio/netbird rather than repointing .gitmodules at a fork: the submodule URL and pin stay upstream, so a plain clone still works and only the SDK build uses the pending code. The step runs before the core-sha step so the cache key follows the commit that is actually built — keying it on the pinned commit would serve a cached framework built without the fix. Remove both steps and re-pin the submodule once #7193 lands. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(ios): resume the login continuation on one queue, publish on the main one Two races in performLogin(), both on the failure paths. The continuation could be resumed twice. `resumed` is a plain captured var with an unsynchronised check-then-set: onOpen resumes from a main-queue block, while the error callback resumed directly on the SDK's goroutine thread. The Go side runs the whole flow on one goroutine — urlOpener.Open() only schedules the main queue block, it does not run it — so a login that fails right after handing over the browser URL (unreachable management server, an immediately rejected token exchange) reaches the error callback while that block is still pending. Both see resumed == false and resume, which traps: resuming a checked continuation twice is a fatal error, not a recoverable one. Move the resume into the error callback's existing main-queue block so both call sites are serialised on the same queue. It goes in a defer rather than at the end of the block: the two guards above it return early — a deallocated self, and an error before the browser phase, which is the path that falls through to the IPC fallback — and neither may leave the await hanging. The IPC fallback then published loginURL and showBrowser straight from performLogin()'s body. That body runs on the cooperative pool: performLogin() is nonisolated, and a nonisolated async function does not inherit its caller's actor, so being called from @MainActor start() does not put it on the main thread. Publishing off the main thread is undefined behaviour in SwiftUI. Hop to the main actor for both, and await the hop — loginIfRequired() reads showBrowser as soon as performLogin() returns, so a fire-and-forget dispatch would reopen the same ordering bug the main-app path documents. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4e08732d80 |
fix(tvOS): reliable device-code login and VPN profile selection (#192)
* Fix tvOS onChange deprecation warnings Update TVMainView to use the tvOS 17-compatible SwiftUI onChange closure forms while preserving the existing state handling behavior. * fix(tvOS): park tunnel start during device authentication IPC for LoginTV only reaches a running extension, but startTunnel used to fail immediately when login was required. Park the start until auth completes, boot the extension from performLogin, and persist the post-login config into the extension-local client. * fix: match VPN configs by provider bundle ID Matching only on localizedDescription can pick up stale VPN profiles from another install. Prefer the current providerBundleIdentifier and remove leftover configs that point at a missing provider. * fix(tvOS): poll login status with a single IPC round-trip TVAuthView's 2s poll timer sent two identical IsLoginComplete messages per tick: checkLoginError decoded LoginDiagnostics for the error, then checkLoginComplete re-fetched the same payload for completion. Replace both adapter methods with a single checkLoginDiagnostics call and move the friendly-error mapping to a LoginDiagnostics.friendlyError property, so error and completion are derived from one atomic response. * fix(tvOS): harden parked login flow * fix(tvOS): address parked login review feedback * fix(tvOS): make auth completion one-shot |
||
|
|
fef31c66fd |
Drop the dedicated server URL file (#202)
* refactor(ios): own profile management in the Go core
Replace the parallel Swift profile manager with a thin wrapper over the
core's ID-based profilemanager.ServiceManager (via the new NetBirdSDK iOS
binding), eliminating the duplicate implementation. Profiles are now
ID-keyed (on-disk filename = id, display name lives in the config); the
default profile stays "default" at the container root and never becomes a
hex id, matching the Go/desktop semantics.
- ProfileManager: thin wrapper over NetBirdSDKProfileManager on iOS; tvOS
keeps its single-default, container-root behavior (no Go profile manager)
- ProfileLayoutMigration: one-time, idempotent, file-coordinated migration
from the legacy profiles/<name>/ directory layout to the Go layout;
preserves auth tokens (copies unparseable configs verbatim), keeps the
active selection and logged-out profiles, and only marks itself done on
full success so a failure retries on the next launch
- ProfileConnectionCache and all callers re-keyed by profile id
- Settings/server changes already target the active profile through the
profile manager (Preferences.configFile -> activeConfigPath)
- Tests: NetBirdTests/ProfileMigrationTests covers the migration, reads it
back through the Go lib, and round-trips a setting via NetBirdSDKPreferences
(the test target now links NetBirdSDK)
- Bump netbird-core submodule to include the iOS profile manager binding
* ci: re-enable Test workflow on push and pull_request
Run the iOS test suite on PRs again to check whether the previously reported test crash still reproduces.
* fix(ios): call gomobile path getters with NSErrorPointer
The non-null String path getters (getActiveConfigPath/getActiveStateFilePath/getConfigPath/getStateFilePath) are generated by gomobile as value-returning calls with an explicit NSErrorPointer parameter, not throwing methods. Wrap them via a goPath helper in ProfileManager (and matching helpers in the tests), fixing the iOS app build failure (missing argument for parameter).
* fix(ios): raise NetBirdTests deployment target to iOS 15.0
The NetBirdTests target was iOS 14.0 while the NetBird app module requires iOS 15.0, so `@testable import NetBird` failed to build ("Compiling for iOS 14.0, but module 'NetBird' has a minimum deployment target of iOS 15.0"). Match the test target to the app so the test bundle compiles and the suite can run.
* ci(ios): use valid GOOGLE_APP_ID in dummy Firebase plist
The dummy GoogleService-Info.plist used a GOOGLE_APP_ID of "dummy",
which fails Firebase's app-ID format validation. At launch the host app
calls FirebaseApp.configure(options:), and +[FIRApp addAppToAppDictionary:]
raised an uncaught NSException, aborting the host before the test runner
could establish a connection ("Test crashed with signal abrt before
establishing connection"). This blocked all hosted tests, not just the
new profile-migration ones.
Give the dummy app ID a well-formed value so configure() succeeds.
* fix(ios): skip Firebase configuration under unit tests
The host app calls FirebaseApp.configure() at launch. When launched purely
to host unit tests, this aborted the process before the test runner could
connect because FirebaseApp.configure() raises an uncaught ObjC exception on
an invalid app ID (the CI dummy plist).
Gate both the iOS and tvOS configure paths on the XCTestConfigurationFilePath
environment variable so Firebase is never initialized in the test host, and
fold the duplicated configure logic into a single helper.
* test(ios): write ManagementURL as Go-style object in migration fixtures
The legacy netbird.cfg is written by the Go SDK, which serializes
Config.ManagementURL (a url.URL) as a nested {Scheme,Host,Path} object,
not a plain string. The settings round-trip test drives NetBirdSDKPreferences,
which fully unmarshals Config and rejected the string fixture with
"cannot unmarshal string into Go struct field Config.ManagementURL".
Normalize the fixture to the object form the SDK actually produces.
* fix(ios): narrow NetBirdTests FRAMEWORK_SEARCH_PATHS to project dir
The test target used a recursive "$(PROJECT_DIR)/**" framework search path,
scanning every subdirectory (DerivedData, netbird-core, package checkouts) and
risking unexpected framework resolution. NetBirdSDK.xcframework lives at the
project root, so point the search path at "$(PROJECT_DIR)" directly.
* chore(ios): bump netbird-core to preserve profile name on auth save
* feat(ios): exclude profile storage from device backups
The profile configs carry the WireGuard and SSH private keys, and the
WireGuard key is the peer's identity: restoring a backup onto another
device would produce two devices connecting with the same key. Mark the
profiles/ directory and the container-root profile files (netbird.cfg,
state.json, active_profile.json, netbird_server_url) as excluded from
iCloud/iTunes backups.
The exclusion is re-applied on every launch from the ProfileManager init:
the Go core rewrites some of these files via temp-file+rename, which drops
per-file attributes. The profiles/ directory is excluded as a whole (and
created up front if missing), so its contents stay covered regardless of
how they are rewritten. After a restore the user re-logs in, matching how
a profile behaves when moved to a new machine on desktop.
* fix(ios): back the profile connection cache with the App Group suite
The cache was backed by UserDefaults.standard, which is per-process: the
network extension is the process that learns the peer's ip/fqdn and runs
the login, so everything it wrote was invisible to the main app and vice
versa. Back it with the App Group suite instead, so both processes see the
same entries.
Entries written by an earlier version are moved over on first use. A suite
that already holds data is never overwritten, so a stale app-local copy
cannot clobber what the extension has since written.
* fix(ios): drop the dedicated server URL file
The file was a fourth copy of the management URL, written next to the
profile config. It existed because Swift code used to rebuild configs and
could drop the URL, leaving login to fall back to the default cloud server
and write that back — so the URL had to be recoverable from somewhere else.
Both reasons are gone. In the ID-based layout the config survives logout
(only the keys are cleared), and the core's DirectUpdateOrCreateConfig
merges into the existing config instead of rebuilding it, so no write can
lose the URL. The connection cache covers the window before the config can
be read back, and now that it lives in the App Group suite it does so
across processes too — which was the file's last unique property.
managementURL(forID:) is therefore config -> connection cache.
saveServerURL(_:forID:) stays as the callers' entry point but records into
the cache. Profiles migrated from the legacy layout keep their URL:
ProfileLayoutMigration already seeds the cache from the legacy file.
The file name stays in the backup exclusion list — earlier versions left
files behind, and those should not reach a backup either.
* fix(ios): resolve the login IPC URL via ProfileManager
login() read the management URL straight from the connection cache when
building the "Login:<configPath>|<statePath>[|<managementURL>]" message,
bypassing ProfileManager.managementURL(forID:), which resolves from the
profile config first and falls back to the cache.
With an empty cache but a live config the URL was omitted from the IPC
message. The practical impact is small — when the config exists the
extension reads the URL from it anyway — but this was the last call site
reading the cache directly for the URL. Going through ProfileManager
keeps both login paths consistent (performLogin already resolves this
way), re-seeds the cache from the config as a side effect, and keeps the
URL in the message even if a cache entry is ever lost.
* fix(ios): merge the connection cache migration instead of clobbering
The standard → App Group suite migration dropped the legacy copy whenever
the suite already held data, and removed it even when the suite write
never landed. Both lose data on the first launch after the upgrade:
- On-demand VPN can start the extension before the app runs, seeding the
suite with URL-only entries; the app's entire legacy cache (ip/fqdn and
URLs for every profile) was then deleted without a merge.
- With a missing App Group entitlement the suite write silently no-ops,
and the standard-domain copy — the last remaining one — was deleted
anyway.
Merge instead: suite values win field by field (the suite is strictly
newer than the legacy copy, which is frozen at upgrade time) and legacy
values fill the fields the suite holds no value for. The standard copy is
removed only after a verified read-back of the merged data — compared as
decoded values, since JSON key order is not stable — so a write that did
not land leaves the copy in place and the migration retries on the next
init.
The migration source is now injectable (defaulting to .standard), and
ProfileConnectionCacheTests covers the merge semantics, the failed-write
path, and idempotency. The migration still inherits the unsynchronized
cross-process read-modify-write on the shared blob; that is addressed
separately.
* fix(ios): store the connection cache under per-profile keys
Every cache write was load → mutate → persist on a single JSON blob.
With the cache in the App Group suite that is a cross-process
read-modify-write with no coordination: the app writing ip/fqdn while
the extension seeds a URL means one side's update is lost to the last
writer.
Split the storage so the schema matches the write patterns: one key per
profile for the connection data (ip/fqdn/ipv6, always written together
by the status updates) and one for the management URL (written by the
login paths). Every write becomes a blind single-key set with no prior
read, so concurrent writers cannot clobber each other's fields and no
cross-process locking is needed — file coordination is unsafe from an
extension, where the other process can be suspended while holding a
claim.
The blob is exploded into the per-profile keys on first use: existing
keys are never overwritten (they are strictly newer than the blob), so
concurrent migration from both processes stays benign, and the blob is
removed only after every entry verifiably landed. The standard → suite
merge keeps writing the blob and this second stage explodes it, so the
full upgrade chain is standard blob → suite blob → per-profile keys.
The public API is unchanged. clearConnectionData now removes the
connection-data key instead of writing empty fields — same observable
behavior, one blind delete. remove clears both keys; a process death
between the two can orphan a URL key, which is harmless and cleaned up
by the next remove for that ID.
|
||
|
|
2768bf2d67 |
Refactor own profile management in the Go core (#147)
* refactor(ios): own profile management in the Go core
Replace the parallel Swift profile manager with a thin wrapper over the
core's ID-based profilemanager.ServiceManager (via the new NetBirdSDK iOS
binding), eliminating the duplicate implementation. Profiles are now
ID-keyed (on-disk filename = id, display name lives in the config); the
default profile stays "default" at the container root and never becomes a
hex id, matching the Go/desktop semantics.
- ProfileManager: thin wrapper over NetBirdSDKProfileManager on iOS; tvOS
keeps its single-default, container-root behavior (no Go profile manager)
- ProfileLayoutMigration: one-time, idempotent, file-coordinated migration
from the legacy profiles/<name>/ directory layout to the Go layout;
preserves auth tokens (copies unparseable configs verbatim), keeps the
active selection and logged-out profiles, and only marks itself done on
full success so a failure retries on the next launch
- ProfileConnectionCache and all callers re-keyed by profile id
- Settings/server changes already target the active profile through the
profile manager (Preferences.configFile -> activeConfigPath)
- Tests: NetBirdTests/ProfileMigrationTests covers the migration, reads it
back through the Go lib, and round-trips a setting via NetBirdSDKPreferences
(the test target now links NetBirdSDK)
- Bump netbird-core submodule to include the iOS profile manager binding
* ci: re-enable Test workflow on push and pull_request
Run the iOS test suite on PRs again to check whether the previously reported test crash still reproduces.
* fix(ios): call gomobile path getters with NSErrorPointer
The non-null String path getters (getActiveConfigPath/getActiveStateFilePath/getConfigPath/getStateFilePath) are generated by gomobile as value-returning calls with an explicit NSErrorPointer parameter, not throwing methods. Wrap them via a goPath helper in ProfileManager (and matching helpers in the tests), fixing the iOS app build failure (missing argument for parameter).
* fix(ios): raise NetBirdTests deployment target to iOS 15.0
The NetBirdTests target was iOS 14.0 while the NetBird app module requires iOS 15.0, so `@testable import NetBird` failed to build ("Compiling for iOS 14.0, but module 'NetBird' has a minimum deployment target of iOS 15.0"). Match the test target to the app so the test bundle compiles and the suite can run.
* ci(ios): use valid GOOGLE_APP_ID in dummy Firebase plist
The dummy GoogleService-Info.plist used a GOOGLE_APP_ID of "dummy",
which fails Firebase's app-ID format validation. At launch the host app
calls FirebaseApp.configure(options:), and +[FIRApp addAppToAppDictionary:]
raised an uncaught NSException, aborting the host before the test runner
could establish a connection ("Test crashed with signal abrt before
establishing connection"). This blocked all hosted tests, not just the
new profile-migration ones.
Give the dummy app ID a well-formed value so configure() succeeds.
* fix(ios): skip Firebase configuration under unit tests
The host app calls FirebaseApp.configure() at launch. When launched purely
to host unit tests, this aborted the process before the test runner could
connect because FirebaseApp.configure() raises an uncaught ObjC exception on
an invalid app ID (the CI dummy plist).
Gate both the iOS and tvOS configure paths on the XCTestConfigurationFilePath
environment variable so Firebase is never initialized in the test host, and
fold the duplicated configure logic into a single helper.
* test(ios): write ManagementURL as Go-style object in migration fixtures
The legacy netbird.cfg is written by the Go SDK, which serializes
Config.ManagementURL (a url.URL) as a nested {Scheme,Host,Path} object,
not a plain string. The settings round-trip test drives NetBirdSDKPreferences,
which fully unmarshals Config and rejected the string fixture with
"cannot unmarshal string into Go struct field Config.ManagementURL".
Normalize the fixture to the object form the SDK actually produces.
* fix(ios): narrow NetBirdTests FRAMEWORK_SEARCH_PATHS to project dir
The test target used a recursive "$(PROJECT_DIR)/**" framework search path,
scanning every subdirectory (DerivedData, netbird-core, package checkouts) and
risking unexpected framework resolution. NetBirdSDK.xcframework lives at the
project root, so point the search path at "$(PROJECT_DIR)" directly.
* chore(ios): bump netbird-core to preserve profile name on auth save
* feat(ios): exclude profile storage from device backups
The profile configs carry the WireGuard and SSH private keys, and the
WireGuard key is the peer's identity: restoring a backup onto another
device would produce two devices connecting with the same key. Mark the
profiles/ directory and the container-root profile files (netbird.cfg,
state.json, active_profile.json, netbird_server_url) as excluded from
iCloud/iTunes backups.
The exclusion is re-applied on every launch from the ProfileManager init:
the Go core rewrites some of these files via temp-file+rename, which drops
per-file attributes. The profiles/ directory is excluded as a whole (and
created up front if missing), so its contents stay covered regardless of
how they are rewritten. After a restore the user re-logs in, matching how
a profile behaves when moved to a new machine on desktop.
* Bump netbird-core submodule to main (ed7d4de99)
|
||
|
|
813bf79255 |
fix(tvOS): use writable local paths for prefs, auth config, and Go state (#191)
* fix(tvOS): use process-local defaults and writable cache paths App-group UserDefaults are non-functional on tvOS, so shared preferences and the cached device name silently failed. Prefer UserDefaults.standard and a writable Caches fallback in DEBUG. * fix(tvOS): use writable scratch path for auth config file NetBirdSDKNewAuth creates or updates netbird.cfg at the given path before the SSO check runs. On tvOS the path came from the App Group container, which is not writable, so changing the management server failed with EPERM before the UserDefaults fallbacks could run. Point configFile() at the caches directory on tvOS instead, mirroring the stateFile() treatment — persistence on tvOS goes through UserDefaults and IPC, not this file. * fix(tvOS): use writable path for Go state file Empty state paths made the state manager write temp files into the extension CWD, which is read-only and failed with EPERM every 10s. * fix(tvOS): prefer caches and validate state path |
||
|
|
c50ff9f24a |
TVos - vpn on demand (#198)
* feat(tvos): connect on demand after reboot and network changes * fix(on-demand): serialize state changes and report failures to callers |
||
|
|
c6a81e0b41 |
Fix/ios reduce login requests (#177)
* cut redundant Login RPCs before Sync Every "is login required?" check is a full Login gRPC call — IsLoginRequired runs doMgmLogin and classifies the error, so there is no cheaper probe. iOS made ~7 per fresh connect against Android's ~3: three layers each verified the auth state before the engine performed the login that actually matters. Removed four duplicates: - client.go Run(): dropped the LoginSync() pre-flight (IsLoginRequired + Login). The engine's loginToManagement is authoritative and runs immediately before the first Sync. Auth failures still surface: PermissionDenied marks the shared status recorder and fires ClientStop → onDisconnected, where needsLoginCached() reports login-required and the provider tears the tunnel down. - login.go: added LoginInteractive, skipping the IsLoginRequired pre-flight for callers that already know login is needed. LoginWithDeviceName keeps the check for tvOS, where the state may be unknown. - PacketTunnelProvider.startTunnel: skips needsLogin() when the main app passes loginVerified in the start options. Starts the main app did not initiate (On Demand, widget) carry no flag and still verify. - ConnectionListener.onDisconnected and restartClient: use the network-free needsLoginCached() instead of the blocking needsLogin(), which cost an RPC on every ordinary disconnect and every network change. Interactive login now issues 3 Login RPCs, a reconnect 2, a network-change restart 1. Go changes are confined to client/ios/NetBirdSDK (ios build tag) — Android and the shared core are untouched. Requires a netbird-core submodule bump and a rebuilt NetBirdSDK.xcframework via ./build-go-lib.sh. Docs: verification scenarios, test-scenario catalog and UI test plan under docs/. * Update .gitmodules * Remove docs * Bump netbird-core to fix/ios-redundant-login-rpcs Points the submodule at the fork branch carrying LoginInteractive and the removed LoginSync pre-flight, which the Swift changes in this branch require. Temporary: revert to the upstream URL and an upstream commit once the core change lands in netbirdio/netbird. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * signal login-required when a restarted engine fails auth The connection listener suppresses its login-required signalling while adapter.isRestarting is true. Normally the stop-phase onDisconnected resets the flag before the restart's engine start begins, so an auth failure there still signals. But when the stop phase never fires onDisconnected (engine already dead) and the stop completion arrives via the 15s fallback timeout instead, isRestarting stays true into the new start and a PermissionDenied failure there died silently: no re-auth signal, no teardown, dead tunnel left black-holing traffic. Re-check the recorder in the start error handler — the engine marks it with PermissionDenied before Run() returns, so the cached check answers without a network call — and signal + cancel the tunnel. Also removes the reliance on the listener's reset ordering. Addresses the CodeRabbit finding on the restart path. --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Zoltán Papp <zoltan.pmail@gmail.com> |
||
|
|
85f865d179 |
fix: preserve peer identity on re-login (#163)
* fix(ios): preserve peer identity on re-login, stop creating duplicate peers Every interactive re-authentication registered a brand-new peer on the management server (named after the fallback hostname) instead of re-using the existing one. Root cause was in the netbird-core iOS SDK: NewAuth built a fresh in-memory config that generated a new WireGuard private key, and performLogin() wrote that config (with the new key) back to netbird.cfg on login success — replacing the peer's persisted identity. Regression from the login flow moving into the main-app process (#138), where OAuth switched from the config-reading IPC path to NewAuth. - Bump netbird-core to fix/ios-relogin: NewAuth now loads the existing config (preserving the private key) when a config file is present, only generating a fresh key for first-time login / after logout when no file exists. - Update the stale performLogin() comment that claimed NewAuth does not read the config file. * fix(ios): log when re-login falls back to the default cloud server managementURL(for:) already recovers the server URL from the config file, the logout-surviving server URL file, and the connection cache in turn, so a nil result means no URL is persisted anywhere. Passing "" to NewAuth is harmless for a re-login now that the SDK reads the existing config (its apply() keeps the config URL unless a non-empty one is provided), but a nil URL on an own-server profile that lost every URL source would silently log in against the default cloud. Log that case so it is visible in diagnostics; keep the "" fallback so a genuine first-time login still works. * fix(ios): register peer under device name, not hostname, on interactive login The interactive OAuth login called the SDK's plain login() path, which passes an empty device name; the management server then registers the peer under the machine hostname fallback instead of the user's device name (UIDevice.current.name). Every other iOS registration path (extension Run/LoginForMobile, setup-key login) already passes Device.getName(), so the same device could appear under two different names depending on the entry point. Switch both interactive login call sites to login(withDeviceName:) with Device.getName(): - NetworkExtensionAdapter.performLogin() (main-app OAuth flow) - NetBirdAdapter.loginAsync() iOS branch (was tvOS-only; now unified since both branches pass the device name) Uses the existing LoginWithDeviceName SDK binding — no netbird-core change needed. Affects first-time registration; a re-login reuses the persisted identity. * Update submodule to the release branch * Fix submodule version * Update submodule to the release branch * Update submodule |
||
|
|
76177e3e5c |
Backport single exit node selection (#151)
* Bump netbird-core: backport single exit-node enforcement Points the submodule at backport/single-exit-node (844881f5c), which carries the exit-node exclusivity reconcile onto v0.74.0 so at most one exit node stays selected. * Enforce single exit-node selection in the routes UI Activating an exit node now deselects every other selected exit node (mirroring the desktop/Wails toggleExitNode behaviour), so 0.0.0.0/0 can't stay pinned to the previously selected peer while the UI shows only the new one. Siblings are deselected before the new node is selected so the core drops the old exit node before adding the new one. Non-exit routes are untouched. * Bump netbird-core: atomic exit-node reconciliation Points the submodule at 415eb59ef, which moves the single exit-node reconcile into one locked RouteSelector method so a concurrent deselect-all can no longer interleave and get undone. * fix(ios): sequence exit-node switch and reconcile route selection selectRoute fired the new selectRoutes before the sibling deselectRoutes round-trips completed. Since both go through independent async sendProviderMessage calls, the select could race the deselects and the core would drop the just-added exit node, leaving 0.0.0.0/0 pinned to the old peer. Deselect every selected exit-node sibling first, track them in a DispatchGroup, and only send the select once all deselects complete. After the select, reconcile the optimistic UI with the core's real state via GetRoutes so a rejected change reverts the toggle instead of leaving a stale selection (Select/Deselect messages don't report their applied result). * fix(ios): guarantee deselectRoutes completion and refresh sibling toggle deselectRoutes returned without calling its completion on the nil-session, send-throw, and UTF-8 branches. selectRoute balances a DispatchGroup around each sibling deselect, so a missed completion stranded the group and the follow-up select/reconcile for the new exit node never ran. Invoke the completion on every exit path. RoutesSelectionInfo.selected is a plain (non-@Published) property, so mutating a sibling emitted no change and its RouteCard toggle stayed on until the next getRoutes reconcile. Send objectWillChange before deselecting the sibling so the toggle flips off immediately, matching the mutually-exclusive exit-node UX. * Update submodule |
||
|
|
43165697a7 |
Clear stale interfaceIPv6 on new engine session (#152)
The settings manager outlives individual engine sessions, and the engine only pushes setInterfaceIPv6 when a session has a v6 address. After IPv6 is disabled or a v4-only profile is selected, a stale v6 address kept createTunnelSettings on the dual-stack branch and skipped the ::/0 blackhole, leaking IPv6 past a selected exit node. Reset interfaceIPv6 at the setInterfaceIP session boundary so a v4-only session can't reuse it. |
||
|
|
5f9df4f391 |
fix(ios): prevent extension crash from late Go callbacks during network flapping (#142)
The packet tunnel extension crash-loops (EXC_BAD_ACCESS, KERN_INVALID_ADDRESS at 0x28) on wifi<->cellular path flapping: it connects briefly, then the extension is killed and relaunched every ~6-12s, so no IP/device name renders and iOS never activates the VPN (T-808). Root cause (code-derived; not yet confirmed against a symbolicated build-7 crash because no dSYM was available): the Go engine retains NetworkChangeListener and DNSManager for the lifetime of the SDK client they were created with. When the adapter is swapped/torn down (profile switch, or a restart triggered by flapping) the old client can still fire a late onNetworkChanged/applyDns into the now-stale listener, which dereferences a tunnel manager whose owning provider has already been deallocated -> use-after-free. Fix: make the Go-held listeners null-safe and explicitly detachable. - NetworkChangeListener / DNSManager: tunnelManager is now optional; every Go callback runs on a serial queue behind a guard (isValid + non-nil tunnelManager). Late/in-flight callbacks become no-ops instead of crashing. Added invalidate() to detach, serialized on the same queue. - NetBirdAdapter: added invalidateListeners(). Deliberately NOT called from stop()/restart (the same adapter is reused there and must keep delivering callbacks) -- only when the adapter is actually discarded. - PacketTunnelProvider: invalidate the outgoing adapter's listeners before replacing it at the two reconfigure sites (startTunnel, handleAppMessage). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
b3721c2a71 |
Fix login flow: wrong server after login, premature auth alert, parallel login (#144)
* Fix login flow: wrong server after login, premature auth alert, parallel login Three distinct bugs in the iOS login flow: 1. Login ran against (and persisted) the default cloud server instead of the user's own management server. performLogin() called NetBirdSDKNewAuth(path, "", nil) with an empty management URL. The Go SDK's NewAuth does not read the config file at the given path — it builds a fresh in-memory config from the URL argument, and an empty URL falls back to DefaultManagementURL (api.netbird.io). Login then ran against the wrong server and DirectWriteOutConfig wrote that wrong server back into the profile's netbird.cfg. Regression from #138, which moved OAuth into the main-app process but passed "" as the URL. Fix: pass the active profile's real management URL (ProfileManager.managementURL) and also persist it to the dedicated, logout-surviving server URL file on login success. 2. The "Login required" alert popped immediately on Connect even for a first-time login. A stale keyLoginRequired flag from a previous extension run was read by the polling timer's checkLoginRequiredFlag() right after connect. Fix: clear the flag (and any pending auth alert) at the start of performConnect(); a genuinely required re-auth is re-signalled by the extension after this connect attempt. 3. The browser login and a parallel VPN start raced. onOpen runs on a background goroutine and resumed the continuation before showBrowser was committed on the main queue, so loginIfRequired() could observe showBrowser == false and call startVPNConnection() — launching the extension, tripping its needsLogin path, and popping the auth alert alongside the browser. Fix: set showBrowser and resume the continuation in the same main-queue block. Also persist the chosen server to the dedicated server URL file on iOS when the user changes the management server in ServerView (previously tvOS-only), so the user's server cannot fall back to the default cloud. * Don't signal login-required when user cancels the login browser Closing the OAuth browser without logging in popped a spurious "Login required" alert and notification. The browser-finished handler called startVPNConnection() unconditionally, which launched the extension, tripped its needs-login path, and signalled login-required — even though the user simply chose not to log in (not a failed active session). The ASWebAuthenticationSession callbackURL cannot distinguish success from cancellation here: NetBird's PKCE flow uses an http://localhost loopback redirect that the Go HTTP server consumes, so the session completes with a nil callbackURL even on success. Determine success from the SDK's onLoginSuccess callback instead: - NetworkExtensionAdapter.loginSucceeded, set by onSuccess (on the main queue) and reset at the start of each performLogin(). - SafariView.didFinish starts the VPN only when loginSucceeded; otherwise it calls the new ViewModel.cancelPendingLogin(), which resets the Connecting state to disconnected and clears the login-required flag and any pending auth alert. * Abort interactive login when the user dismisses the browser Closing the OAuth browser without logging in left the SDK's PKCE loopback HTTP server bound to its redirect port (the iOS login runs in the main-app process and nothing cancelled it). The next connect then stalled in "Connecting…" while the new login flow tried to bind the same still-occupied port, eventually failed, and only later managed to reopen the browser. cancelPendingLogin() now calls the new NetworkExtensionAdapter.cancelLogin(), which stops the SDK auth (Auth.Stop, added in the netbird-core submodule). Stopping cancels the auth context, unblocking WaitToken and shutting down its loopback server so the port is freed at once. Mirrors Android, where stopEngine() aborts login via the engine context. Bumps netbird-core submodule to fix/ios-auth-stop-cancel for Auth.Stop(). NOTE: requires regenerating NetBirdSDK.xcframework on a machine with Xcode (./build-go-lib.sh) so the Swift binding exposes Auth.stop(); the binding cannot be built on Linux. |
||
|
|
3ccb66a821 |
Replace Lottie connect button with toggle switch (#140)
* feat(ios): replace Lottie connect button with toggle switch - Add VPNToggleView: custom pill toggle with spring animation and pulse during connecting/disconnecting states - Optimistic UI: thumb moves instantly on tap, pulse starts before OS confirms - Simplify iOSConnectionView: monotone BgMenu background, centered layout, SECURED/NOT SECURED status label, hostname/IP grouped under toggle * Update iOSConnectionView.swift * Update iOSConnectionView.swift * feat(ios): redesign connection screen with toggle switch and expandable IP details * Update main screen UI * Fix padding and tap area for main screen |
||
|
|
07e90dc209 |
Notify user when VPN disconnects due to re-authentication (#106)
* feat: notify user when VPN disconnects due to re-authentication * Fix double notification * Update project.pbxproj * Update netbird-core |
||
|
|
a448f5ea6e |
Propagate exit-node deselect to synthesized v6 (::/0) route (#128)
* Update submodule
* Update submodule
* Update submodule
* [ui] fix exit-node route indicator stuck on yellow
A merged exit-node route card exposes its network as a comma-joined string
("0.0.0.0/0, ::/0"), while peer.routes lists each prefix as a separate entry.
statusIndicatorColor compared the whole joined string with connectedPeerRoutes
.contains(...), which can never equal a single entry, so the exit node always
showed yellow even when the peer was connected. Non-merged routes (a single
prefix) matched fine, which is why only the exit node looked wrong.
Split route.network on "," and match any component, so the indicator turns
green when the peer is connected on either the v4 or v6 default route.
* [ui] use core-computed route status for the indicator
Read the new Status field from the bridge and color the route indicator from
it (Connected -> green, else yellow), instead of inferring status by matching
the network string against peer routes. This fixes the merged exit node always
showing yellow, because its network value is the joined "0.0.0.0/0, ::/0" which
never equaled a single peer-route entry.
Threads status through RoutesSelectionInfo and the PacketTunnelProvider mapping.
Keeps the legacy network-matching path (now split-aware) as a fallback for cores
that don't yet provide a status.
* [ui] drop legacy network-matching from route indicator
Now that the core always provides a per-route connection status, the route
indicator no longer needs to infer it from peer routes. Remove the fallback
network-string matching (and the resolved-IP check), so statusIndicatorColor
simply reflects route.status. Also drops the now-unused peerViewModel
dependency from RouteCard and its call site.
* [deps] bump netbird-core to exit-node v6 deselect + route status fixes
Points the submodule at the netbird-core commits that fix the orphaned v6 exit
selection (leaking ::/0 after the v4 exit node was disabled) and compute the
per-route connection status in the iOS bridge.
* [ui] fix stale comment on RoutesSelectionInfo.status
The status property no longer has a legacy network-matching fallback (removed
when the indicator switched to reading route.status directly). Update the doc
comment to describe the current behavior: consumers use the core-provided status
directly, and a nil/empty value is treated as not-connected (yellow indicator).
* Update submodule
* Update submodule
* Update submodule
* Update submodule
|