Skip to content

fix(ios): stop forcing VPN on-demand so the native toggle works#2262

Open
0443n wants to merge 1 commit into
hiddify:mainfrom
0443n:feat/ios-connect-on-demand-setting
Open

fix(ios): stop forcing VPN on-demand so the native toggle works#2262
0443n wants to merge 1 commit into
hiddify:mainfrom
0443n:feat/ios-connect-on-demand-setting

Conversation

@0443n

@0443n 0443n commented Jun 24, 2026

Copy link
Copy Markdown

What's wrong

On iOS, flipping the system VPN switch off (Settings > VPN, or Control Center) while you're connected through Hiddify doesn't actually stop the tunnel. It disconnects and comes right back. So the native on/off control does nothing.

I ran into this on my own device, I haven't seen anyone else report it. But it looks like a bug rather than intended behavior, and the cause is clear, so I figured it was worth a PR.

Why

On every connect, VPNManager.enableVPNManager() installs an on-demand rule and turns it on:

let rule = NEOnDemandRuleConnect()
rule.interfaceTypeMatch = .any
rule.probeURL = URL(string: "http://captive.apple.com")
manager.onDemandRules = [rule]
manager.isOnDemandEnabled = true

NEOnDemandRuleConnect with interfaceTypeMatch = .any tells iOS the tunnel should always be up. Flip the native switch off and iOS re-checks the rule and reconnects straight away. The only way around it is digging into the VPN profile in Settings and turning off "Connect On Demand" by hand.

The change

Stop forcing on-demand. enableVPNManager() now clears the rules and turns it off, so the native toggle does what people expect. I set it to false rather than just deleting the lines so the next connect also wipes any on-demand state left behind by an older build. The cleanup already in disconnect() stays, since it covers the case where someone updates the app while the tunnel is running.

manager.onDemandRules = []
manager.isOnDemandEnabled = false

This is iOS only. Android has no on-demand logic (always-on VPN is an OS-level setting there), and macOS runs sing-box as a process with no NetworkExtension, so neither one is affected.

One thing I want to check with you

I'd rather raise this than decide it on my own. That on-demand rule was also acting as a reconnect / kill switch: if the tunnel drops, iOS brings it back so traffic doesn't go out in the clear. For a circumvention tool that's worth something. The catch is that it was on with no way to turn it off, and it broke the native switch.

Most tap-to-connect iOS VPN apps don't force on-demand, so I just removed it here. If you'd rather keep that safety net, I'm happy to make it a setting instead: off by default so the native switch works, on for anyone who wants the kill switch. Tell me which way you want it and I'll redo it.

Testing

I couldn't build it locally (no iOS toolchain on my machine). The change only touches enableVPNManager() and leaves the rest of the connect/disconnect path alone.

@0443n

0443n commented Jun 24, 2026

Copy link
Copy Markdown
Author

Follow-up after digging into how iOS actually handles this.

I wanted to check whether users could just manage "Connect On Demand" from iOS Settings themselves, instead of the app deciding it in code. Turns out that toggle on its own doesn't do anything. Per Apple's docs and DTS, on-demand needs two things together: isOnDemandEnabled = true and a non-empty onDemandRules array. The Settings toggle only flips the boolean. If the app hasn't installed any rules, turning it on has no effect, since there's nothing for the system to match against.

So "let the user set it in Settings" isn't really a workaround. The rules have to come from the app. A couple of consequences:

  • This patch clears the rules and the boolean, so it removes on-demand for good, including the ability to opt back in from Settings. If someone wants the auto-reconnect behavior, the only way to bring it back is an in-app option (the setting I mentioned), not the iOS toggle.
  • Edge case: anyone upgrading from a current build already has the rule persisted, so their Settings toggle works right now. This patch wipes it on the next connect. That's the intended effect of dropping on-demand, just flagging it as a behavior change for them.

Doesn't change my recommendation, but it does mean an opt-in setting is the only way to keep on-demand available at all. Happy to switch to that if you'd prefer.

Sources:

@0443n

0443n commented Jun 25, 2026

Copy link
Copy Markdown
Author

@veto9292 sorry for the ping - mind taking a look at this one when you get a chance? it's a small iOS-only change (one file). no rush, and i'm happy to rework it into an opt-in setting instead if you'd rather keep on-demand around.

@0443n

0443n commented Jun 26, 2026

Copy link
Copy Markdown
Author

@hiddify-com could you take a look when you have a sec? small one-file iOS fix. thanks!

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.

1 participant