Skip to content

Comments

TUN inbound: Enhance Darwin interface support#5598

Merged
RPRX merged 2 commits intoXTLS:mainfrom
Owersun:tun-darwin-rebase
Jan 25, 2026
Merged

TUN inbound: Enhance Darwin interface support#5598
RPRX merged 2 commits intoXTLS:mainfrom
Owersun:tun-darwin-rebase

Conversation

@Owersun
Copy link
Collaborator

@Owersun Owersun commented Jan 23, 2026

  • reduce number of actions done to create/configure the interface in the system,
  • assign synthetic static link-local ip address to the interface, that is required by the OS to just be available for routing,
  • make tun_darwin_endpoint be implemented significantly more similar to tun_windows_enpoint, preparing them for potential unification,
  • added MacOSX specific README.md section,

I checked the functionality on my local machine to work, checking every element make sense, and functions call proper ioctl's.

@RPRX
Copy link
Member

RPRX commented Jan 24, 2026

#5559 (comment) 提到简单改一下就能在 iOS 上跑,可以并入这个 PR

@evozi-team 测试一下这个 PR 对于 iOS 有改进吗

@Owersun
Copy link
Collaborator Author

Owersun commented Jan 24, 2026

I was planning to check how this can be extended for iOS support as a next step.
Was planning to have it as further enhancement PR later. This is just "de-vibing" the initial Darwin support, and making it as close as possible to how windows/linux is working.

@RPRX
Copy link
Member

RPRX commented Jan 24, 2026

那这个 PR 不急着合并,还有五天的时间,有新的 changes 直接放这里就行

@RPRX
Copy link
Member

RPRX commented Jan 24, 2026

可以先 rebase 445c0d4 ,方便 @evozi-team 测下 iOS

@RPRX
Copy link
Member

RPRX commented Jan 24, 2026

现在的 TUN 还剩个 macOS 上的进程名路由 #5559 (comment) 和三个平台上的 auto-toute #5594 (comment) 就差不多了

v2rayN 那边可能都是直接用那两家的 auto-route 而不是自己操作路由表的

@Owersun
Copy link
Collaborator Author

Owersun commented Jan 25, 2026

I've investigated the iOS support.
An iOS application build with a golang part, is a process when native XCode iOS app is created and then bind to some golang app (https://go.dev/wiki/Mobile#building-and-deploying-to-ios-1)
Meaning the main application in this case is some XCode project create app, and golang is "included" in it as some kind of external library.
Using tunnel interface in this case require main XCode app to create a tunnel through native iOS methods, and then pass it to the golang app through bind function as a memory structure with some methods. Then golang app can start the loop lifecycle around that passed structure reading and writing into it.

This all sounds to me, like to have real iOS support I need to see at least one real life implementation of an iOS app that uses XRay-Core as that kind of "bind" application/library.
And then I can design tun_ios.go around what that application is going to have.
I already has "sample" implementation around very generic ios NEPacketTunnelProvider structure, but it has no "native" ios main app it should be included in, so I can't even test it or say that it works in any proper way.
It would be nice, if people who requested iOS support would point me to their repository where the "main" app that plans to use XRay-Core that way is. Then I can tie them together and test.

Besides that the darwin/macos enhancement, which also includes unification with windows enpoint (because they really work similar), is done in this pull request.
If I'd have a link to iOS app that uses XRay-Core as golang library, I can evaluate how good iOS support can be, if not - this PR can be merged as is, and I can do iOS later.

@RPRX
Copy link
Member

RPRX commented Jan 25, 2026

Rebase 一下吧,ready 了就合并

@Owersun
Copy link
Collaborator Author

Owersun commented Jan 25, 2026

Rebased on top of main.
There are two commits in this branch, I'd like to keep them separate, as they do different things. One is darwin enhancements, the other is unification of windows/darwin endpoint. They are independent.

@RPRX
Copy link
Member

RPRX commented Jan 25, 2026

看起来还是没有 rebase

- reduce number of actions done to create/configure the interface in the system
- assign synthetic static link-local ipv4/ipv6 addresses to the interface, that are required by the OS for the routing to work
- make tun_darwin_endpoint be implemented significantly more similar to tun_windows_enpoint, preparing them for potential unification
…milar, into one GVisorEndpoint.

Making darwin/windows tun implement GVisorDevice with simple readpacket/writepacket methods that GVisorEndpoint untilise
@Owersun
Copy link
Collaborator Author

Owersun commented Jan 25, 2026

Something went wrong with all the force updates and syncs between main and Owersun/main. I refreshed everything and pushed everything clean anew. Now there is exactly two commits on top of current main

@RPRX RPRX changed the title Proxy: TUN: Enhance Darwin interface support TUN inbound: Enhance Darwin interface support Jan 25, 2026
@RPRX RPRX merged commit 5173e5c into XTLS:main Jan 25, 2026
39 checks passed
@dereference23
Copy link

@Owersun is it possible to have split tunneling with this on macOS? I'm not a network guy but I tried to do my best. This is what I did (en1 here is my Wi-Fi interface):

  1. sudo route add -host <VLESS IP>/32 <en1's gateway IP>
  2. sudo route delete default
  3. sudo route add default -interface utun20

In my xray config, I have added "sockopt": { "interface": "en1" } to the outbounds. It works perfectly with VLESS routes but doesn't work with "protocol": "freedom" routes. I also tried sendThrough instead of sockopt.interface, the result was the same. And I guess in theory it shouldn't be needed to do step 1 when specifying interface but it doesn't work like that too. Maybe there is something I'm missing here

@Owersun
Copy link
Collaborator Author

Owersun commented Jan 25, 2026

Hey. I think you always need step 1.
And other way around, specifying the interface will not really help. Xray binary will fire out outbound traffic to the OS, where OS will route it normally, and having outbound interface there will not really make it route properly without step 1 you did.
So to me steps 1/2/3 look just right and enough to make it work. I honestly tested it very similar, just not with a default gateway, but some limited ip block.
What do you mean "works perfectly with VLESS routes but doesn't work with "protocol": "freedom" routes"? As described above, what Xray consider "protocol: freedom" just means that it will dump the packet to the OS to route. Where it will hit back the Xray through tun, as the default route points there.
If you want have something like split routing, where half your traffic goes through VLESS outbounds, half just through default route, I'm afraid all those "freedom" routes has to have to be added same way step 1 is done - through default gateway ip. That's just the nature of this setup - tun interface is network abstraction, which is subject of OS routing, Xray application level abstraction, that in the end will always initiate network traffic back on the OS level, it will just be wrapped into some layers in case it decides it has to be under VLESS.
So when you make "protocol: freedom" route, you can imagine like Xray doesn't exist in the middle, the traffic will enter the routing normally and exit the routing normally.

I'll try to investigate the topic about are there ways to have like multiple routing tables in MacOSX, like linux has, but recently dealing with it's network stack I doubt they cared about that... I'll write here what I found.

@evozi-team
Copy link
Contributor

evozi-team commented Jan 26, 2026

可以先 rebase 445c0d4 ,方便 @evozi-team 测下 iOS

PR iOS 5612

内存不会一直往下掉了,可用内存稳定在20-30mb之间。
比较稳了 但一直测速还是会炸

@Owersun
Copy link
Collaborator Author

Owersun commented Jan 26, 2026

可以先 rebase 445c0d4 ,方便 @evozi-team 测下 iOS

PR iOS 5612

内存不会一直往下掉了,可用内存稳定在20-30mb之间。 比较稳了 但一直测速还是会炸

What I saw is that people add this trying to contain the iOS constraints:

func init() {
	// iOS NetworkExtension memory limit is ~15MB
	// Go runtime takes ~5MB, we need to be aggressive with GC

	// Run GC more frequently (default is 100)
	debug.SetGCPercent(10)

	// Set soft memory limit to 12MB to leave headroom
	debug.SetMemoryLimit(12 << 20)

	// Periodically return memory to OS
	go func() {
		ticker := time.NewTicker(30 * time.Second)
		defer ticker.Stop()
		for range ticker.C {
			debug.FreeOSMemory()
		}
	}()
}

@RPRX
Copy link
Member

RPRX commented Jan 26, 2026

现在 iOS 限制是 50MB 了,赶紧发邮件让库克改成 100MB 解决所有问题

@evozi-team
Copy link
Contributor

可以先 rebase 445c0d4 ,方便 @evozi-team 测下 iOS

PR iOS 5612
内存不会一直往下掉了,可用内存稳定在20-30mb之间。 比较稳了 但一直测速还是会炸

What I saw is that people add this trying to contain the iOS constraints:

func init() {
	// iOS NetworkExtension memory limit is ~15MB
	// Go runtime takes ~5MB, we need to be aggressive with GC

	// Run GC more frequently (default is 100)
	debug.SetGCPercent(10)

	// Set soft memory limit to 12MB to leave headroom
	debug.SetMemoryLimit(12 << 20)

	// Periodically return memory to OS
	go func() {
		ticker := time.NewTicker(30 * time.Second)
		defer ticker.Stop()
		for range ticker.C {
			debug.FreeOSMemory()
		}
	}()
}

已经用了这个~ https://github.com/XTLS/libXray/blob/main/memory/memory_ios.go

@Owersun
Copy link
Collaborator Author

Owersun commented Jan 26, 2026

If you have few back traces and reasons of crashes that point to golang tun part, so its clear that it happens because of that code, I can think about why it could be happening.

nebulabox added a commit to nebulabox/Xray-core that referenced this pull request Jan 28, 2026
* commit 'f6a7e939231e5ec6b167628bf730dc70a3c36707': (90 commits)
  VMess inbound: Optimize replay filter (XTLS#5562)
  Bump github.com/pires/go-proxyproto from 0.9.1 to 0.9.2 (XTLS#5614)
  TUN inbound: Add iOS support (XTLS#5612)
  Geodat: Reduce peak memory usage (XTLS#5581)
  Bump github.com/pires/go-proxyproto from 0.9.0 to 0.9.1 (XTLS#5608)
  Hysteria transport: Support range & random for `interval` in `udphop` as well (XTLS#5603)
  TUN inbound: Enhance Darwin interface support (XTLS#5598)
  XUDP client: Initialize Global ID's BaseKey correctly (XTLS#5602)
  TUN inbound: Disable RACK/TLP recovery to fix connection stalls (XTLS#5600)
  v26.1.23
  common/errors/feature_errors.go: Add PrintNonRemovalDeprecatedFeatureWarning() (XTLS#5567)
  API: Add ListRule() for routing (XTLS#5569)
  Log config: More flexible `maskAddress` (XTLS#5570)
  Bump github.com/miekg/dns from 1.1.70 to 1.1.72 (XTLS#5590)
  Bump github.com/cloudflare/circl from 1.6.2 to 1.6.3 (XTLS#5589)
  Hysteria transport: Fix speedtest issue (XTLS#5587)
  README.md: Add fancyss to Asuswrt-Merlin Clients
  Router: Fix panic in ProcessNameMatcher when source IPs are empty (XTLS#5574)
  README.md: Update links for PassWall & PassWall 2 (XTLS#5572)
  Tests: Reduce RAM usage (XTLS#5577)
  ...

# Conflicts:
#	core/core.go
@dereference23
Copy link

Hey. I think you always need step 1. And other way around, specifying the interface will not really help. Xray binary will fire out outbound traffic to the OS, where OS will route it normally, and having outbound interface there will not really make it route properly without step 1 you did. So to me steps 1/2/3 look just right and enough to make it work. I honestly tested it very similar, just not with a default gateway, but some limited ip block. What do you mean "works perfectly with VLESS routes but doesn't work with "protocol": "freedom" routes"? As described above, what Xray consider "protocol: freedom" just means that it will dump the packet to the OS to route. Where it will hit back the Xray through tun, as the default route points there. If you want have something like split routing, where half your traffic goes through VLESS outbounds, half just through default route, I'm afraid all those "freedom" routes has to have to be added same way step 1 is done - through default gateway ip. That's just the nature of this setup - tun interface is network abstraction, which is subject of OS routing, Xray application level abstraction, that in the end will always initiate network traffic back on the OS level, it will just be wrapped into some layers in case it decides it has to be under VLESS. So when you make "protocol: freedom" route, you can imagine like Xray doesn't exist in the middle, the traffic will enter the routing normally and exit the routing normally.

I'll try to investigate the topic about are there ways to have like multiple routing tables in MacOSX, like linux has, but recently dealing with it's network stack I doubt they cared about that... I'll write here what I found.

Hi, thanks for your reply. I want to let you know I have achieved split-tunneling with a little trick.
So:

  1. For the outbounds specify either sockopt.interface or sendThrough in config (I prefer interface because the IP is dynamic)
  2. Add routes excluding 0.0.0.0/8
sudo route -n add -net 1.0.0.0/8 -interface utun20
sudo route -n add -net 2.0.0.0/7 -interface utun20
sudo route -n add -net 4.0.0.0/6 -interface utun20
sudo route -n add -net 8.0.0.0/5 -interface utun20
sudo route -n add -net 16.0.0.0/4 -interface utun20
sudo route -n add -net 32.0.0.0/3 -interface utun20
sudo route -n add -net 64.0.0.0/2 -interface utun20
sudo route -n add -net 128.0.0.0/1 -interface utun20

With these routes, the freedom outbound is accessible and there is also no need to add the VLESS host route. As you have probably guessed, the trick is in avoiding 0.0.0.0/8 in the routing table. Otherwise, that route would be shown above the default route in netstat -nr.

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.

4 participants