Skip to content

Feat/add support for forcing device auth flow on ios#4944

Merged
shuuri-labs merged 10 commits intomainfrom
feat/add-support-for-forcing-device-auth-flow-on-ios
Dec 30, 2025
Merged

Feat/add support for forcing device auth flow on ios#4944
shuuri-labs merged 10 commits intomainfrom
feat/add-support-for-forcing-device-auth-flow-on-ios

Conversation

@shuuri-labs
Copy link
Copy Markdown
Contributor

@shuuri-labs shuuri-labs commented Dec 12, 2025

Describe your changes

This PR adds support for forcing device authentication flow on iOS/tvOS, enabling Apple TV (tvOS) support similar to what was done for Android TV.

Background

tvOS (Apple TV) cannot use the standard PKCE authentication flow because:

  1. Safari is not available on Apple TV
  2. The tvOS sandbox blocks atomic file operations (temp file + rename) in App Group containers
  3. Shared App Group UserDefaults doesn't sync between the main app and Network Extension on tvOS

This implementation uses the device code flow where users scan a QR code or enter a code on another device to authenticate.


SDK Changes (client/ios/NetBirdSDK/)

login.go

  • URLOpener interface - Extended with userCode parameter and OnLoginSuccess() callback
  • SSOListener / ErrListener - New async listener interfaces for mobile framework
  • SaveConfigIfSSOSupported - Changed from sync (bool, error) to async with SSOListener callback
  • LoginWithSetupKeyAndSaveConfig - Changed from sync error to async with ErrListener callback
  • Login() renamed to LoginSync() - Original sync method renamed, now used for background VPN without UI
  • New Login() - Interactive login with forceDeviceAuth flag
  • New LoginWithDeviceName() - Interactive login with custom device name support (required for tvOS)
  • GetConfigJSON() / SetConfigFromJSON() - New methods for alternative config storage (UserDefaults on tvOS)
  • DirectWriteOutConfig usage - All file writes use non-atomic operations for tvOS compatibility
  • Nil guards - Added nil checks for listener interfaces to prevent panics
  • Config save ordering - Config saved before OnLoginSuccess callback to ensure persistence
  • Timeout contexts - Added bounded timeouts for RequestAuthInfo calls

client.go

  • IsLoginRequired - Improved error handling, returns true on config load failure
  • LoginForMobile - Added timeout context for auth info request

device_ios.go

  • ErrInvalidTunnelFD - New exported error for invalid tunnel file descriptor
  • tunFd == 0 handling - Now a hard error instead of fallback (no viable fallback on iOS/tvOS)

config.go (profilemanager)

  • DirectUpdateOrCreateConfig - Added permission enforcement for existing config files
  • ConfigFromJSON - Now applies defaults after unmarshaling

Breaking API Changes

These are intentional breaking changes for the iOS SDK to support tvOS:

  1. URLOpener interface: Open(string)Open(url, userCode) + OnLoginSuccess()
  2. SaveConfigIfSSOSupported: Sync → Async with listener
  3. LoginWithSetupKeyAndSaveConfig: Sync → Async with listener
  4. Login(): Renamed to LoginSync(), new Login() has different signature

The iOS client app has been updated to use these new APIs. Existing iOS functionality is preserved.

Issue ticket number and link

Stack

Checklist

  • Is it a bug fix
  • Is a typo/documentation fix
  • Is a feature enhancement
  • It is a refactor
  • Created tests that fail without the change (if possible)

By submitting this pull request, you confirm that you have read and agree to the terms of the Contributor License Agreement.

Documentation

Select exactly one:

  • I added/updated documentation for this change
  • Documentation is not needed for this change (explain why)

Docs PR URL (required if "docs added" is checked)

Summary by CodeRabbit

  • New Features

    • tvOS in-memory preloaded config with JSON import/export and a synchronous LoginSync.
  • New Behavior

    • Reworked login orchestration: device-name-aware interactive flows, foreground OAuth/device flow with bounded timeouts, and UI notification on successful login.
    • tvOS now prefers in-memory/config paths that avoid atomic file operations.
  • Bug Fixes

    • Improved detection and clearer error reporting when tunnel/socket initialization fails on iOS/tvOS.

✏️ Tip: You can customize this high-level summary in your review settings.

- Remove verbose debug logging from WaitToken in device_flow.go
- Improve TUN FD=0 fallback comments and warning messages
- Document why config save after login differs from Android
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Dec 12, 2025

Walkthrough

Adds tvOS-friendly non-atomic config read/write and in-memory preloaded config, refactors iOS/tvOS login to listener-driven flows and a changed URLOpener interface, and hardens iOS TUN creation by treating tunFd == 0 as an explicit error and guarding fd-based TUN construction.

Changes

Cohort / File(s) Summary
tvOS Config Persistence
client/internal/profilemanager/config.go
Added DirectWriteOutConfig, DirectUpdateOrCreateConfig, ConfigToJSON, and ConfigFromJSON to support non-atomic JSON (de)serialization and direct writes for environments that block atomic rename-based updates.
Client Preloaded Config
client/ios/NetBirdSDK/client.go
Added preloadedConfig *profilemanager.Config and SetConfigFromJSON(jsonStr string) error; Run, IsLoginRequired, and mobile login paths prefer in-memory config and use DirectUpdateOrCreateConfig when needed.
iOS/tvOS Login Flow Refactor
client/ios/NetBirdSDK/login.go
Reworked login flows to listener/async patterns, added LoginSync, LoginWithDeviceName, foregroundGetTokenInfo, GetConfigJSON/SetConfigFromJSON, switched persistence to DirectWriteOutConfig, and updated URLOpener to Open(url string, userCode string) plus OnLoginSuccess().
Preferences Commit Path
client/ios/NetBirdSDK/preferences.go
Replaced UpdateOrCreateConfig with DirectUpdateOrCreateConfig in Commit() to avoid atomic rename-based writes on tvOS App Group containers.
TUN Device Handling
client/iface/device/device_ios.go
Added exported ErrInvalidTunnelFD, validate tunFd == 0 as invalid, initialize and guard tunDevice creation path; preserve fd-dup + non-blocking + fd-based creation for non-zero tunFd, otherwise use fallback creation path.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant App as App/UI
    participant Client as Client
    participant Auth as Authenticator
    participant URLOpener as URLOpener
    participant Profile as ProfileManager
    participant TUN as TunDevice

    Note over Client,Profile: Run() / config selection
    App->>Client: Run()
    alt preloadedConfig present
        Client->>Client: use preloadedConfig
    else
        Client->>Profile: DirectUpdateOrCreateConfig()
    end

    Note over Client,Auth: Login orchestration
    Client->>Auth: LoginSync() or Login(resultListener,...)
    Auth->>URLOpener: Open(url, userCode)
    URLOpener-->>App: show URL + userCode
    URLOpener->>Auth: OnLoginSuccess()
    Auth->>Auth: foregroundGetTokenInfo (poll / wait)
    Auth->>Profile: DirectWriteOutConfig(cfg)

    Note over Client,TUN: TUN creation
    Client->>TUN: TunDevice.Create(tunFd)
    alt tunFd == 0
        TUN->>TUN: create fallback TUN (CreateTUN)
    else tunFd != 0
        TUN->>TUN: dup fd -> set O_NONBLOCK -> create from fd
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Review focus:
    • client/ios/NetBirdSDK/login.go — listener conversions, URLOpener signature change, token wait/timeouts, and post-login persistence.
    • client/internal/profilemanager/config.go — correctness of non-atomic write/update and JSON round-trip semantics.
    • client/iface/device/device_ios.go — safety around tunFd == 0 handling, fd duplication, non-blocking flags, and cleanup.

Possibly related PRs

Suggested reviewers

  • pascal-fischer
  • pappz

Poem

🐇
A hop for tvOS, configs held tight,
Async codes glowing on the login light.
Direct saves hum, no rename in sight,
TUN checks the FD, falls back if not right.
VPN dreams stitched, snug through the night.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Feat/add support for forcing device auth flow on ios' accurately reflects the main change: adding device authentication flow support for iOS/tvOS platforms.
Description check ✅ Passed PR description comprehensively covers changes, rationale, breaking API changes, and documentation status aligned with template requirements.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/add-support-for-forcing-device-auth-flow-on-ios

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.

@shuuri-labs shuuri-labs marked this pull request as ready for review December 12, 2025 16:39
Copy link
Copy Markdown
Contributor

@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: 3

🧹 Nitpick comments (2)
client/ios/NetBirdSDK/client.go (1)

268-272: Consider logging config load errors for debugging.

Similar to IsLoginRequired, the error from DirectUpdateOrCreateConfig is ignored. While subsequent operations would fail if cfg is nil, logging the error would help with debugging tvOS issues.

 	// Use DirectUpdateOrCreateConfig to avoid atomic file operations (temp file + rename)
 	// which are blocked by the tvOS sandbox in App Group containers
-	cfg, _ := profilemanager.DirectUpdateOrCreateConfig(profilemanager.ConfigInput{
+	cfg, err := profilemanager.DirectUpdateOrCreateConfig(profilemanager.ConfigInput{
 		ConfigPath: c.cfgFile,
 	})
+	if err != nil {
+		log.Errorf("LoginForMobile: failed to load config: %v", err)
+		return err.Error()
+	}
client/ios/NetBirdSDK/login.go (1)

273-276: Inconsistent context usage with context.TODO().

RequestAuthInfo uses context.TODO() while the rest of the method uses a.ctx. This inconsistency means cancellation of a.ctx won't propagate to RequestAuthInfo, which could lead to resource leaks or unexpected behavior.

-	flowInfo, err := oAuthFlow.RequestAuthInfo(context.TODO())
+	flowInfo, err := oAuthFlow.RequestAuthInfo(a.ctx)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 932c02e and f76f762.

📒 Files selected for processing (5)
  • client/iface/device/device_ios.go (1 hunks)
  • client/internal/profilemanager/config.go (2 hunks)
  • client/ios/NetBirdSDK/client.go (5 hunks)
  • client/ios/NetBirdSDK/login.go (8 hunks)
  • client/ios/NetBirdSDK/preferences.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
client/ios/NetBirdSDK/preferences.go (1)
client/internal/profilemanager/config.go (1)
  • DirectUpdateOrCreateConfig (825-840)
client/ios/NetBirdSDK/client.go (1)
client/internal/profilemanager/config.go (4)
  • Config (90-161)
  • ConfigFromJSON (876-883)
  • DirectUpdateOrCreateConfig (825-840)
  • ConfigInput (47-87)
client/internal/profilemanager/config.go (1)
util/file.go (2)
  • DirectWriteJson (59-95)
  • ReadJson (183-202)
client/ios/NetBirdSDK/login.go (6)
client/android/login.go (4)
  • Auth (40-44)
  • SSOListener (21-24)
  • ErrListener (27-30)
  • URLOpener (34-37)
client/internal/pkce_auth.go (1)
  • GetPKCEAuthorizationFlowInfo (52-118)
client/internal/profilemanager/config.go (3)
  • DirectWriteOutConfig (819-821)
  • ConfigToJSON (866-872)
  • ConfigFromJSON (876-883)
client/internal/login.go (2)
  • Login (51-84)
  • IsLoginRequired (21-48)
client/system/info.go (1)
  • DeviceNameCtxKey (16-16)
client/internal/auth/oauth.go (1)
  • TokenInfo (45-53)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (19)
  • GitHub Check: Client / Unit (amd64)
  • GitHub Check: Client (Docker) / Unit
  • GitHub Check: Management / Unit (amd64, mysql)
  • GitHub Check: Management / Unit (amd64, postgres)
  • GitHub Check: Management / Unit (amd64, sqlite)
  • GitHub Check: Relay / Unit (386)
  • GitHub Check: Relay / Unit (amd64, -race)
  • GitHub Check: Client / Unit (386)
  • GitHub Check: release_ui
  • GitHub Check: release
  • GitHub Check: release_ui_darwin
  • GitHub Check: Linux
  • GitHub Check: Windows
  • GitHub Check: Darwin
  • GitHub Check: JS / Lint
  • GitHub Check: Android / Build
  • GitHub Check: Client / Unit
  • GitHub Check: Client / Unit
  • GitHub Check: Client / Unit
🔇 Additional comments (8)
client/internal/profilemanager/config.go (1)

863-883: LGTM!

The JSON serialization helpers are clean and follow the existing patterns in the codebase. They provide the necessary functionality for tvOS alternative storage mechanisms.

client/ios/NetBirdSDK/preferences.go (1)

112-116: LGTM!

The switch to DirectUpdateOrCreateConfig is appropriate for tvOS sandbox compatibility, and the comment clearly explains the rationale.

client/ios/NetBirdSDK/client.go (1)

94-106: LGTM!

The SetConfigFromJSON method provides a clean way to load preloaded config for tvOS with proper error handling and logging.

client/ios/NetBirdSDK/login.go (5)

34-37: LGTM!

The updated URLOpener interface with userCode parameter and OnLoginSuccess callback aligns with the Android SDK and enables proper device auth flow support for tvOS.


75-119: LGTM!

The async refactoring of SaveConfigIfSSOSupported follows a clean pattern with proper error propagation to the listener. The fallback from PKCE to Device Authorization flow handles the tvOS use case correctly.


154-185: LGTM!

LoginSync correctly handles the background VPN scenario where the user should already be authenticated. Returning an error when needsLogin is true is the right behavior for this non-interactive context.


187-192: LGTM!

Good use of the deprecation pattern - the old Login method delegates to the new LoginWithDeviceName with sensible defaults, maintaining backward compatibility while encouraging migration.


300-319: LGTM!

The JSON config methods provide clean integration with alternative storage mechanisms. The nil check in GetConfigJSON is appropriate defensive coding.

Comment thread client/iface/device/device_ios.go
Comment thread client/internal/profilemanager/config.go
Comment thread client/ios/NetBirdSDK/client.go
Copy link
Copy Markdown
Contributor

@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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
client/ios/NetBirdSDK/login.go (2)

20-37: Document/guarantee callback threading for SSOListener/ErrListener/URLOpener (currently background goroutines).
All callbacks are invoked from goroutines (not the caller thread). If the iOS/tvOS UI layer expects main-thread calls, this needs to be explicitly documented (or dispatched). Also consider nil-guarding listener / urlOpener to avoid SDK panics on misuse.


122-152: Same cfgPath hazard in loginWithSetupKeyAndSaveConfig + consider passing ctxWithValues into withBackOff.
If a.cfgPath is empty this will fail at the end; that’s fine, but make it explicit/early to avoid “login succeeded but save failed” surprises. Also, withBackOff(a.ctx, ...) could take ctxWithValues instead to keep one context for cancellation/propagation.

 func (a *Auth) loginWithSetupKeyAndSaveConfig(setupKey string, deviceName string) error {
@@
-	err := a.withBackOff(a.ctx, func() error {
+	err := a.withBackOff(ctxWithValues, func() error {
 		backoffErr := internal.Login(ctxWithValues, a.config, setupKey, "")
@@
 	}
 
+	if a.cfgPath == "" {
+		return fmt.Errorf("cfgPath is empty")
+	}
+
 	// Use DirectWriteOutConfig to avoid atomic file operations (temp file + rename)
 	// which are blocked by the tvOS sandbox in App Group containers
 	return profilemanager.DirectWriteOutConfig(a.cfgPath, a.config)
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f76f762 and 285086e.

📒 Files selected for processing (1)
  • client/ios/NetBirdSDK/login.go (8 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (18)
  • GitHub Check: Relay / Unit (amd64, -race)
  • GitHub Check: Relay / Unit (386)
  • GitHub Check: Management / Unit (amd64, postgres)
  • GitHub Check: Management / Unit (amd64, mysql)
  • GitHub Check: Management / Unit (amd64, sqlite)
  • GitHub Check: Client (Docker) / Unit
  • GitHub Check: Client / Unit (amd64)
  • GitHub Check: Client / Unit (386)
  • GitHub Check: release_ui_darwin
  • GitHub Check: release
  • GitHub Check: Client / Unit
  • GitHub Check: Darwin
  • GitHub Check: Windows
  • GitHub Check: Linux
  • GitHub Check: Android / Build
  • GitHub Check: JS / Lint
  • GitHub Check: Client / Unit
  • GitHub Check: Client / Unit
🔇 Additional comments (3)
client/ios/NetBirdSDK/login.go (3)

154-185: LoginSync() behavior looks consistent for “background connect only” usage.
Returning not authenticated when IsLoginRequired is true is a clear contract for callers.


301-320: GetConfigJSON / SetConfigFromJSON LGTM—please treat returned JSON as sensitive.
Since this likely contains private key material/tokens, callers should avoid logging it and ensure storage is protected (Keychain-encrypted storage / appropriate tvOS protections).


75-119: Guard cfgPath == "" before calling DirectWriteOutConfig (currently can fail unexpectedly).

saveConfigIfSSOSupported() always writes to a.cfgPath; but if NewAuthWithConfig doesn't set cfgPath, this can error even when SSO probing succeeds. Either (a) return a clear error like "cfgPath is empty", or (b) skip writing when empty and rely on GetConfigJSON() persistence. Verify the intended behavior when PKCE is unavailable but device flow is available.

 func (a *Auth) saveConfigIfSSOSupported() (bool, error) {
 	if err != nil {
 		return false, fmt.Errorf("backoff cycle failed: %v", err)
 	}
 
+	if a.cfgPath == "" {
+		return false, fmt.Errorf("cfgPath is empty")
+	}
+
 	// Use DirectWriteOutConfig to avoid atomic file operations (temp file + rename)
 	// which are blocked by the tvOS sandbox in App Group containers
 	err = profilemanager.DirectWriteOutConfig(a.cfgPath, a.config)
 	return true, err
 }

Comment thread client/ios/NetBirdSDK/login.go
Comment thread client/ios/NetBirdSDK/login.go
- Remove goroutine from OnLoginSuccess callback, invoke synchronously
- Stop treating PermissionDenied as success, propagate as permanent error
- Replace context.TODO() with bounded timeout context (30s) in RequestAuthInfo
- Handle DirectUpdateOrCreateConfig errors in IsLoginRequired and LoginForMobile
- Add permission enforcement to DirectUpdateOrCreateConfig for existing configs
- Fix variable shadowing in device_ios.go where err was masked by := in else block
Copy link
Copy Markdown
Contributor

@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: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
client/ios/NetBirdSDK/client.go (2)

75-106: Guard preloadedConfig with a mutex (current code is race-prone).

SetConfigFromJSON() writes c.preloadedConfig, while Run() / IsLoginRequired() read it without any synchronization.

 type Client struct {
@@
 	connectClient         *internal.ConnectClient
 	// preloadedConfig holds config loaded from JSON (used on tvOS where file writes are blocked)
 	preloadedConfig       *profilemanager.Config
+	preloadedConfigMu     sync.RWMutex
 }
@@
 func (c *Client) SetConfigFromJSON(jsonStr string) error {
@@
-	c.preloadedConfig = cfg
+	c.preloadedConfigMu.Lock()
+	c.preloadedConfig = cfg
+	c.preloadedConfigMu.Unlock()
@@
 }
@@
-	if c.preloadedConfig != nil {
+	c.preloadedConfigMu.RLock()
+	preloaded := c.preloadedConfig
+	c.preloadedConfigMu.RUnlock()
+	if preloaded != nil {
 		log.Infof("Run: using preloaded config from memory")
-		cfg = c.preloadedConfig
+		cfg = preloaded
 	} else {
@@
-	if c.preloadedConfig != nil {
+	c.preloadedConfigMu.RLock()
+	preloaded := c.preloadedConfig
+	c.preloadedConfigMu.RUnlock()
+	if preloaded != nil {
 		log.Infof("IsLoginRequired: using preloaded config from memory")
-		cfg = c.preloadedConfig
+		cfg = preloaded
 	} else {

Also applies to: 112-131, 236-270


311-330: loginComplete is also written/read across goroutines without synchronization.

LoginForMobile() sets c.loginComplete = true in a goroutine, while IsLoginComplete() / ClearLoginComplete() access it without locks/atomics. Consider sync/atomic (or a small mutex) for this flag.

Also applies to: 332-338

♻️ Duplicate comments (3)
client/iface/device/device_ios.go (1)

51-89: Good fix for the prior err shadowing; fallback path is clear and separated.

The dupTunFd, err = unix.Dup(...) pattern avoids masking errors from the fd-based path (matches the earlier concern).

client/internal/profilemanager/config.go (1)

823-846: Good: permission enforcement parity with UpdateOrCreateConfig.

client/ios/NetBirdSDK/login.go (1)

208-246: Use the same ctx (with deviceName + cancellation) throughout the OAuth device-flow.

login() builds ctx, but foregroundGetTokenInfo() uses a.ctx, so timeouts/cancellation/value propagation diverge.

-		tokenInfo, err := a.foregroundGetTokenInfo(urlOpener, forceDeviceAuth)
+		tokenInfo, err := a.foregroundGetTokenInfo(ctx, urlOpener, forceDeviceAuth)
@@
-func (a *Auth) foregroundGetTokenInfo(urlOpener URLOpener, forceDeviceAuth bool) (*auth.TokenInfo, error) {
-	oAuthFlow, err := auth.NewOAuthFlow(a.ctx, a.config, false, forceDeviceAuth, "")
+func (a *Auth) foregroundGetTokenInfo(ctx context.Context, urlOpener URLOpener, forceDeviceAuth bool) (*auth.TokenInfo, error) {
+	oAuthFlow, err := auth.NewOAuthFlow(ctx, a.config, false, forceDeviceAuth, "")
@@
-	authInfoCtx, authInfoCancel := context.WithTimeout(a.ctx, authInfoRequestTimeout)
+	authInfoCtx, authInfoCancel := context.WithTimeout(ctx, authInfoRequestTimeout)
@@
-	waitCTX, cancel := context.WithTimeout(a.ctx, waitTimeout)
+	waitCTX, cancel := context.WithTimeout(ctx, waitTimeout)

Also applies to: 268-294

🧹 Nitpick comments (1)
client/internal/profilemanager/config.go (1)

817-867: Direct (non-atomic) writes are fine for tvOS constraints, but document/mitigate partial-write risk.

Since DirectWriteJson can leave a truncated file if the process is killed mid-write, consider at least (a) a comment warning callers that recovery may require re-auth, and/or (b) adding a cheap “write then fsync” inside util.DirectWriteJson if feasible for the target container.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 285086e and b96d41e.

📒 Files selected for processing (4)
  • client/iface/device/device_ios.go (1 hunks)
  • client/internal/profilemanager/config.go (2 hunks)
  • client/ios/NetBirdSDK/client.go (6 hunks)
  • client/ios/NetBirdSDK/login.go (7 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
client/ios/NetBirdSDK/client.go (2)
client/internal/profilemanager/config.go (4)
  • Config (90-161)
  • ConfigFromJSON (882-889)
  • DirectUpdateOrCreateConfig (825-846)
  • ConfigInput (47-87)
client/internal/login.go (2)
  • IsLoginRequired (21-48)
  • Login (51-84)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (19)
  • GitHub Check: Management / Unit (amd64, mysql)
  • GitHub Check: Client / Unit (amd64)
  • GitHub Check: Relay / Unit (386)
  • GitHub Check: Relay / Unit (amd64, -race)
  • GitHub Check: Management / Unit (amd64, postgres)
  • GitHub Check: Client (Docker) / Unit
  • GitHub Check: Client / Unit (386)
  • GitHub Check: Management / Unit (amd64, sqlite)
  • GitHub Check: release
  • GitHub Check: release_ui_darwin
  • GitHub Check: Windows
  • GitHub Check: Client / Unit
  • GitHub Check: Linux
  • GitHub Check: Darwin
  • GitHub Check: Client / Unit
  • GitHub Check: Android / Build
  • GitHub Check: iOS / Build
  • GitHub Check: JS / Lint
  • GitHub Check: Client / Unit
🔇 Additional comments (2)
client/ios/NetBirdSDK/client.go (1)

236-270: Nice hardening of IsLoginRequired around config load failures / nil config.

client/ios/NetBirdSDK/login.go (1)

154-186: LoginSync() behavior and error messaging look coherent for “no-UI” background flows.

Comment thread client/iface/device/device_ios.go Outdated
Comment thread client/internal/profilemanager/config.go
Comment thread client/ios/NetBirdSDK/login.go
Comment thread client/ios/NetBirdSDK/login.go Outdated
Comment thread client/ios/NetBirdSDK/login.go Outdated
- Make tunFd == 0 a hard error with exported ErrInvalidTunnelFD (remove dead fallback code)
- Apply defaults in ConfigFromJSON to prevent partially-initialized configs
- Add nil guards for listener/urlOpener interfaces in public SDK entry points
- Reorder config save before OnLoginSuccess to prevent teardown race
- Add explanatory comment for urlOpener.Open goroutine
Copy link
Copy Markdown
Contributor

@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: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
client/iface/device/device_ios.go (1)

71-90: Fix os.NewFile(...) + unix.Close(dupTunFd) double-close hazard.

The code creates an *os.File wrapping dupTunFd and passes it to CreateTUNFromFile (line 85). According to wireguard-go documentation, CreateTUNFromFile takes ownership of the file descriptor — the Device will close it when Device.Close() is called.

If CreateTUNFromFile fails (line 86–90), the code calls unix.Close(dupTunFd) directly. However, the *os.File created at line 85 still exists and has a finalizer that will close the same fd when garbage collected. This creates a double-close hazard.

Extract the file to a named variable and call f.Close() on error instead:

-	tunDevice, err = tun.CreateTUNFromFile(os.NewFile(uintptr(dupTunFd), "/dev/tun"), 0)
+	f := os.NewFile(uintptr(dupTunFd), "/dev/tun")
+	tunDevice, err = tun.CreateTUNFromFile(f, 0)
 	if err != nil {
 		log.Errorf("Unable to create new tun device from fd: %v", err)
-		_ = unix.Close(dupTunFd)
+		_ = f.Close()
 		return nil, err
 	}
♻️ Duplicate comments (1)
client/ios/NetBirdSDK/login.go (1)

284-316: Don’t launch urlOpener.Open(...) in a goroutine (ordering + UX races).
This was flagged in earlier review and it’s still present: WaitToken polling can begin before the UI is actually presented, and Swift gets less deterministic ordering guarantees. Prefer calling Open synchronously; if Swift needs main-thread work, dispatch there in Swift.

-	go urlOpener.Open(flowInfo.VerificationURIComplete, flowInfo.UserCode)
+	urlOpener.Open(flowInfo.VerificationURIComplete, flowInfo.UserCode)

Also recommended: pass the ctx created in login() down into foregroundGetTokenInfo so cancellation/values stay consistent (and you don’t silently drop deviceName context values).

🧹 Nitpick comments (3)
client/ios/NetBirdSDK/login.go (3)

20-37: URLOpener + listener interfaces look consistent with device-flow UX needs.
One request: ensure the SDK docs clearly state which callbacks can be invoked on background goroutines (most of these do).


90-123: SSO probing via PKCE then Device Flow + DirectWriteOutConfig is sensible for tvOS constraints.
Minor: consider whether returning (true, err) on save failure is what callers expect, vs (false, err) (since you couldn’t persist the “SSO-supported config”).


162-194: LoginSync semantics are clearer now (fails fast when interactive auth is required).
Optional: consider exporting a sentinel error (like you did with ErrInvalidTunnelFD) so Swift can branch on “needs interactive auth” without string matching.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b96d41e and 25934e0.

📒 Files selected for processing (3)
  • client/iface/device/device_ios.go (3 hunks)
  • client/internal/profilemanager/config.go (2 hunks)
  • client/ios/NetBirdSDK/login.go (7 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • client/internal/profilemanager/config.go
🧰 Additional context used
🧬 Code graph analysis (1)
client/iface/device/device_ios.go (2)
shared/management/status/error.go (1)
  • Errorf (70-75)
relay/testec2/tun/tun.go (1)
  • Device (13-21)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (20)
  • GitHub Check: Client / Unit (amd64)
  • GitHub Check: Client (Docker) / Unit
  • GitHub Check: Management / Unit (amd64, sqlite)
  • GitHub Check: Management / Unit (amd64, postgres)
  • GitHub Check: Management / Unit (amd64, mysql)
  • GitHub Check: Client / Unit (386)
  • GitHub Check: Relay / Unit (amd64, -race)
  • GitHub Check: Relay / Unit (386)
  • GitHub Check: JS / Lint
  • GitHub Check: release
  • GitHub Check: release_ui_darwin
  • GitHub Check: release_ui
  • GitHub Check: Darwin
  • GitHub Check: Windows
  • GitHub Check: Linux
  • GitHub Check: Client / Unit
  • GitHub Check: Client / Unit
  • GitHub Check: iOS / Build
  • GitHub Check: Android / Build
  • GitHub Check: Client / Unit
🔇 Additional comments (6)
client/iface/device/device_ios.go (1)

49-69: Hard-error on tunFd == 0 is the right move (clearer failure mode for iOS/tvOS NE fd wiring).
This also resolves the earlier “fallback TUN” ambiguity and should make tvOS failures much more diagnosable.

client/ios/NetBirdSDK/login.go (5)

75-88: Nil-guard + async wrapper for SaveConfigIfSSOSupported is good defensive SDK hygiene.


126-160: Async LoginWithSetupKeyAndSaveConfig with backoff.Permanent on PermissionDenied is a good UX optimization.


203-223: LoginWithDeviceName nil-guards and async result delivery look solid for Swift bindings.


225-282: Good: persist config before OnLoginSuccess() and call success synchronously.
This directly avoids the teardown-order race noted in earlier reviews.


327-346: Config JSON helpers are a good escape hatch for tvOS/UserDefaults persistence.

@sonarqubecloud
Copy link
Copy Markdown

Copy link
Copy Markdown
Contributor

@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: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
client/ios/NetBirdSDK/client.go (2)

64-109: Guard preloadedConfig with a mutex (current Set/Read is racy).
Line 78–80 + Line 100–108 + Line 121–135: SetConfigFromJSON() can run concurrently with Run() / IsLoginRequired() and races on c.preloadedConfig.

diff --git a/client/ios/NetBirdSDK/client.go b/client/ios/NetBirdSDK/client.go
@@
 import (
 	"context"
 	"fmt"
 	"net/netip"
 	"os"
 	"sort"
 	"strings"
 	"sync"
+	"sync/atomic"
 	"time"
@@
 type Client struct {
@@
 	connectClient         *internal.ConnectClient
 	// preloadedConfig holds config loaded from JSON (used on tvOS where file writes are blocked)
 	preloadedConfig       *profilemanager.Config
+	preloadedConfigMu     sync.RWMutex
+	loginComplete         uint32 // accessed concurrently (goroutine in LoginForMobile)
 }
@@
 func (c *Client) SetConfigFromJSON(jsonStr string) error {
 	cfg, err := profilemanager.ConfigFromJSON(jsonStr)
 	if err != nil {
 		log.Errorf("SetConfigFromJSON: failed to parse config JSON: %v", err)
 		return err
 	}
-	c.preloadedConfig = cfg
+	c.preloadedConfigMu.Lock()
+	c.preloadedConfig = cfg
+	c.preloadedConfigMu.Unlock()
 	log.Infof("SetConfigFromJSON: config loaded successfully from JSON")
 	return nil
 }
@@
 	// Use preloaded config if available (tvOS where file writes are blocked)
-	if c.preloadedConfig != nil {
+	c.preloadedConfigMu.RLock()
+	preloaded := c.preloadedConfig
+	c.preloadedConfigMu.RUnlock()
+	if preloaded != nil {
 		log.Infof("Run: using preloaded config from memory")
-		cfg = c.preloadedConfig
+		cfg = preloaded
 	} else {

Also applies to: 120-135


228-274: Don’t hold ctxCancelLock across network calls; it blocks Stop() from canceling.
Line 236–273 and Line 287–321: defer c.ctxCancelLock.Unlock() keeps the lock held for the full login-required check / auth-info request, so Stop() can’t acquire the lock to call c.ctxCancel().

diff --git a/client/ios/NetBirdSDK/client.go b/client/ios/NetBirdSDK/client.go
@@
 func (c *Client) IsLoginRequired() bool {
@@
-	c.ctxCancelLock.Lock()
-	defer c.ctxCancelLock.Unlock()
-	ctx, c.ctxCancel = context.WithCancel(ctxWithValues)
+	c.ctxCancelLock.Lock()
+	ctx, cancel := context.WithCancel(ctxWithValues)
+	c.ctxCancel = cancel
+	c.ctxCancelLock.Unlock()
+	defer cancel()
@@
-	if c.preloadedConfig != nil {
+	c.preloadedConfigMu.RLock()
+	preloaded := c.preloadedConfig
+	c.preloadedConfigMu.RUnlock()
+	if preloaded != nil {
 		log.Infof("IsLoginRequired: using preloaded config from memory")
-		cfg = c.preloadedConfig
+		cfg = preloaded
 	} else {
@@
 func (c *Client) LoginForMobile() string {
@@
-	c.ctxCancelLock.Lock()
-	defer c.ctxCancelLock.Unlock()
-	ctx, c.ctxCancel = context.WithCancel(ctxWithValues)
+	c.ctxCancelLock.Lock()
+	ctx, cancel := context.WithCancel(ctxWithValues)
+	c.ctxCancel = cancel
+	c.ctxCancelLock.Unlock()
+	// NOTE: don't defer cancel here; the goroutine below uses ctx. Stop() should cancel it.
@@
 	go func() {
+		defer cancel()
 		waitTimeout := time.Duration(flowInfo.ExpiresIn) * time.Second
 		waitCTX, cancel := context.WithTimeout(ctx, waitTimeout)
 		defer cancel()
@@
-		c.loginComplete = true
+		atomic.StoreUint32(&c.loginComplete, 1)
 	}()
@@
 func (c *Client) IsLoginComplete() bool {
-	return c.loginComplete
+	return atomic.LoadUint32(&c.loginComplete) == 1
 }
@@
 func (c *Client) ClearLoginComplete() {
-	c.loginComplete = false
+	atomic.StoreUint32(&c.loginComplete, 0)
 }

Also applies to: 279-334

client/ios/NetBirdSDK/login.go (1)

92-125: Fail fast (clear error) when cfgPath is empty before calling DirectWriteOutConfig.
Line 121–124 and Line 159–162: if a.cfgPath == "", the write will fail with a relatively opaque filesystem error; returning a clearer error helps SDK consumers.

diff --git a/client/ios/NetBirdSDK/login.go b/client/ios/NetBirdSDK/login.go
@@
 func (a *Auth) saveConfigIfSSOSupported() (bool, error) {
@@
-	err = profilemanager.DirectWriteOutConfig(a.cfgPath, a.config)
+	if a.cfgPath == "" {
+		return false, fmt.Errorf("cfgPath is empty; cannot persist config")
+	}
+	err = profilemanager.DirectWriteOutConfig(a.cfgPath, a.config)
 	return true, err
 }
@@
 func (a *Auth) loginWithSetupKeyAndSaveConfig(setupKey string, deviceName string) error {
@@
-	return profilemanager.DirectWriteOutConfig(a.cfgPath, a.config)
+	if a.cfgPath == "" {
+		return fmt.Errorf("cfgPath is empty; cannot persist config")
+	}
+	return profilemanager.DirectWriteOutConfig(a.cfgPath, a.config)
 }

Also applies to: 143-162

🧹 Nitpick comments (4)
client/ios/NetBirdSDK/preferences.go (1)

114-118: Switch to non-atomic config update path makes sense; tweak comment to match build-tag/platform reality.
Line 115–117: Using DirectUpdateOrCreateConfig aligns with the App Group atomic-rename constraint. Only nit: consider rewording the comment to “iOS/tvOS App Group containers” (or whatever the ios build tag actually covers in this repo) to avoid confusion.

client/ios/NetBirdSDK/client.go (1)

291-299: Consider using the preloaded-config path in LoginForMobile() for tvOS parity.
Line 291–299: LoginForMobile() always hits DirectUpdateOrCreateConfig even if preloadedConfig is set, which differs from Run() / IsLoginRequired(). If tvOS uses this path, it’ll likely break.

client/ios/NetBirdSDK/login.go (2)

227-314: Use the same ctx (deviceName + cancellation) for foregroundGetTokenInfo, not a.ctx.
Line 231–262 vs Line 289–307: login() correctly switches to a deviceName-enriched ctx, but foregroundGetTokenInfo() uses a.ctx, so the interactive flow may ignore cancellation and context values intended for this login attempt.

diff --git a/client/ios/NetBirdSDK/login.go b/client/ios/NetBirdSDK/login.go
@@
-		tokenInfo, err := a.foregroundGetTokenInfo(urlOpener, forceDeviceAuth)
+		tokenInfo, err := a.foregroundGetTokenInfo(ctx, urlOpener, forceDeviceAuth)
@@
-func (a *Auth) foregroundGetTokenInfo(urlOpener URLOpener, forceDeviceAuth bool) (*auth.TokenInfo, error) {
-	oAuthFlow, err := auth.NewOAuthFlow(a.ctx, a.config, false, forceDeviceAuth, "")
+func (a *Auth) foregroundGetTokenInfo(ctx context.Context, urlOpener URLOpener, forceDeviceAuth bool) (*auth.TokenInfo, error) {
+	if ctx == nil {
+		ctx = context.Background()
+	}
+	oAuthFlow, err := auth.NewOAuthFlow(ctx, a.config, false, forceDeviceAuth, "")
 	if err != nil {
 		return nil, err
 	}
@@
-	authInfoCtx, authInfoCancel := context.WithTimeout(a.ctx, authInfoRequestTimeout)
+	authInfoCtx, authInfoCancel := context.WithTimeout(ctx, authInfoRequestTimeout)
@@
-	waitCTX, cancel := context.WithTimeout(a.ctx, waitTimeout)
+	waitCTX, cancel := context.WithTimeout(ctx, waitTimeout)

325-344: Config JSON API is fine; ensure callers treat it as sensitive.
Line 325–343: GetConfigJSON() will include private key material and other secrets from profilemanager.Config; worth a short doc note that it must be stored securely (Keychain/UserDefaults protections/etc.).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 202b6f6 and 57564ab.

📒 Files selected for processing (3)
  • client/ios/NetBirdSDK/client.go (6 hunks)
  • client/ios/NetBirdSDK/login.go (7 hunks)
  • client/ios/NetBirdSDK/preferences.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
client/ios/NetBirdSDK/preferences.go (1)
client/internal/profilemanager/config.go (1)
  • DirectUpdateOrCreateConfig (825-846)
client/ios/NetBirdSDK/client.go (2)
client/internal/profilemanager/config.go (4)
  • Config (90-161)
  • ConfigFromJSON (883-897)
  • DirectUpdateOrCreateConfig (825-846)
  • ConfigInput (47-87)
client/internal/login.go (2)
  • IsLoginRequired (21-48)
  • Login (51-84)
client/ios/NetBirdSDK/login.go (5)
client/android/login.go (3)
  • Auth (40-44)
  • SSOListener (21-24)
  • URLOpener (34-37)
client/internal/profilemanager/config.go (3)
  • DirectWriteOutConfig (819-821)
  • ConfigToJSON (872-878)
  • ConfigFromJSON (883-897)
client/internal/login.go (2)
  • Login (51-84)
  • IsLoginRequired (21-48)
client/internal/auth/oauth.go (2)
  • TokenInfo (45-53)
  • NewOAuthFlow (74-86)
management/server/http/testing/testing_tools/tools.go (1)
  • ExpiresIn (43-43)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (19)
  • GitHub Check: Client / Unit (386)
  • GitHub Check: Management / Unit (amd64, mysql)
  • GitHub Check: Management / Unit (amd64, postgres)
  • GitHub Check: Relay / Unit (386)
  • GitHub Check: Relay / Unit (amd64, -race)
  • GitHub Check: Client (Docker) / Unit
  • GitHub Check: Management / Unit (amd64, sqlite)
  • GitHub Check: Client / Unit (amd64)
  • GitHub Check: release_ui_darwin
  • GitHub Check: release
  • GitHub Check: JS / Lint
  • GitHub Check: Linux
  • GitHub Check: Darwin
  • GitHub Check: Windows
  • GitHub Check: Client / Unit
  • GitHub Check: Android / Build
  • GitHub Check: Client / Unit
  • GitHub Check: Client / Unit
  • GitHub Check: iOS / Build

@shuuri-labs shuuri-labs merged commit 96cdd56 into main Dec 30, 2025
36 of 38 checks passed
@shuuri-labs shuuri-labs deleted the feat/add-support-for-forcing-device-auth-flow-on-ios branch December 30, 2025 16:41
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.

3 participants