tailscale: Add system interface support

This commit is contained in:
世界
2026-01-07 14:37:58 +08:00
parent bd9935eebb
commit f196b7a583
8 changed files with 458 additions and 14 deletions

View File

@@ -4,8 +4,11 @@ icon: material/new-box
!!! quote "Changes in sing-box 1.13.0"
:material-plus: [relay_server_port](#relay_server_port)
:material-plus: [relay_server_port](#relay_server_port)
:material-plus: [relay_server_static_endpoints](#relay_server_static_endpoints)
:material-plus: [system_interface](#system_interface)
:material-plus: [system_interface_name](#system_interface_name)
:material-plus: [system_interface_mtu](#system_interface_mtu)
!!! question "Since sing-box 1.12.0"
@@ -27,8 +30,11 @@ icon: material/new-box
"advertise_exit_node": false,
"relay_server_port": 0,
"relay_server_static_endpoints": [],
"system_interface": false,
"system_interface_name": "",
"system_interface_mtu": 0,
"udp_timeout": "5m",
... // Dial Fields
}
```
@@ -98,12 +104,34 @@ Indicates whether the node should advertise itself as an exit node.
#### relay_server_port
!!! question "Since sing-box 1.13.0"
The port to listen on for incoming relay connections from other Tailscale nodes.
#### relay_server_static_endpoints
!!! question "Since sing-box 1.13.0"
Static endpoints to advertise for the relay server.
#### system_interface
!!! question "Since sing-box 1.13.0"
Create a system TUN interface for Tailscale.
#### system_interface_name
!!! question "Since sing-box 1.13.0"
Custom TUN interface name. By default, `tailscale` (or `utun` on macOS) will be used.
#### system_interface_mtu
!!! question "Since sing-box 1.13.0"
Override the TUN MTU. By default, Tailscale's own MTU is used.
#### udp_timeout
UDP NAT expiration time.

View File

@@ -4,8 +4,11 @@ icon: material/new-box
!!! quote "sing-box 1.13.0 中的更改"
:material-plus: [relay_server_port](#relay_server_port)
:material-plus: [relay_server_port](#relay_server_port)
:material-plus: [relay_server_static_endpoints](#relay_server_static_endpoints)
:material-plus: [system_interface](#system_interface)
:material-plus: [system_interface_name](#system_interface_name)
:material-plus: [system_interface_mtu](#system_interface_mtu)
!!! question "自 sing-box 1.12.0 起"
@@ -27,6 +30,9 @@ icon: material/new-box
"advertise_exit_node": false,
"relay_server_port": 0,
"relay_server_static_endpoints": [],
"system_interface": false,
"system_interface_name": "",
"system_interface_mtu": 0,
"udp_timeout": "5m",
... // 拨号字段
@@ -97,12 +103,34 @@ icon: material/new-box
#### relay_server_port
!!! question "自 sing-box 1.13.0 起"
监听来自其他 Tailscale 节点的中继连接的端口。
#### relay_server_static_endpoints
!!! question "自 sing-box 1.13.0 起"
为中继服务器通告的静态端点。
#### system_interface
!!! question "自 sing-box 1.13.0 起"
为 Tailscale 创建系统 TUN 接口。
#### system_interface_name
!!! question "自 sing-box 1.13.0 起"
自定义 TUN 接口名。默认使用 `tailscale`macOS 上为 `utun`)。
#### system_interface_mtu
!!! question "自 sing-box 1.13.0 起"
覆盖 TUN 的 MTU。默认使用 Tailscale 自己的 MTU。
#### udp_timeout
UDP NAT 过期时间。
@@ -115,4 +143,4 @@ UDP NAT 过期时间。
Tailscale 端点中的拨号字段仅控制它如何连接到控制平面,与实际连接无关。
参阅 [拨号字段](/zh/configuration/shared/dial/) 了解详情。
参阅 [拨号字段](/zh/configuration/shared/dial/) 了解详情。

7
go.mod
View File

@@ -38,10 +38,10 @@ require (
github.com/sagernet/sing-shadowsocks v0.2.8
github.com/sagernet/sing-shadowsocks2 v0.2.1
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11
github.com/sagernet/sing-tun v0.8.0-beta.11.0.20251230194736-a5db80d71081
github.com/sagernet/sing-tun v0.8.0-beta.11.0.20260107060547-525f783d005b
github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1
github.com/sagernet/smux v1.5.50-sing-box-mod.1
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.4
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.5
github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20250917110311-16510ac47288
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854
github.com/spf13/cobra v1.10.2
@@ -68,6 +68,7 @@ require (
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/caddyserver/zerossl v0.1.3 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 // indirect
github.com/database64128/netx-go v0.1.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect
@@ -85,6 +86,7 @@ require (
github.com/google/btree v1.1.3 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/yamux v0.1.2 // indirect
github.com/hdevalence/ed25519consensus v0.2.0 // indirect
@@ -131,6 +133,7 @@ require (
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 // indirect
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a // indirect
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 // indirect
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc // indirect
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 // indirect
github.com/tidwall/gjson v1.18.0 // indirect

17
go.sum
View File

@@ -22,6 +22,8 @@ github.com/cilium/ebpf v0.15.0 h1:7NxJhNiBT3NG8pZJ3c+yfrVdHY8ScgKD27sScgjLMMk=
github.com/cilium/ebpf v0.15.0/go.mod h1:DHp1WyrLeiBh19Cf/tfiSMhqheEiK8fXFZ4No0P1Hso=
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 h1:8h5+bWd7R6AYUslN6c6iuZWTKsKxUFDlpnmilO6R2n0=
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/cretz/bine v0.2.0 h1:8GiDRGlTgz+o8H9DSnsl+5MeBK4HsExxgl6WgzOCuZo=
github.com/cretz/bine v0.2.0/go.mod h1:WU4o9QR9wWp8AVKtTM1XD5vUHkEqnf2vVSo6dBqbetI=
@@ -79,6 +81,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 h1:wG8RYIyctLhdFk6Vl1yPGtSRtwGpVkWyZww1OCil2MI=
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=
@@ -216,14 +220,14 @@ github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnq
github.com/sagernet/sing-shadowsocks2 v0.2.1/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ=
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 h1:tK+75l64tm9WvEFrYRE1t0YxoFdWQqw/h7Uhzj0vJ+w=
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA=
github.com/sagernet/sing-tun v0.8.0-beta.11.0.20251230194736-a5db80d71081 h1:ZFw+y1RIKasXENuy8jOYfwpyiKBh92HcSqzDFQLd7Yc=
github.com/sagernet/sing-tun v0.8.0-beta.11.0.20251230194736-a5db80d71081/go.mod h1:+HAK/y9GZljdT0KYKMYDR8MjjqnqDDQZYp5ZZQoRzS8=
github.com/sagernet/sing-tun v0.8.0-beta.11.0.20260107060547-525f783d005b h1:MqPEFejgxqechqBn1OkL+9JPW0W4AiGCI0Y1JhglIlQ=
github.com/sagernet/sing-tun v0.8.0-beta.11.0.20260107060547-525f783d005b/go.mod h1:+HAK/y9GZljdT0KYKMYDR8MjjqnqDDQZYp5ZZQoRzS8=
github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 h1:aSwUNYUkVyVvdmBSufR8/nRFonwJeKSIROxHcm5br9o=
github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1/go.mod h1:P11scgTxMxVVQ8dlM27yNm3Cro40mD0+gHbnqrNGDuY=
github.com/sagernet/smux v1.5.50-sing-box-mod.1 h1:XkJcivBC9V4wBjiGXIXZ229aZCU1hzcbp6kSkkyQ478=
github.com/sagernet/smux v1.5.50-sing-box-mod.1/go.mod h1:NjhsCEWedJm7eFLyhuBgIEzwfhRmytrUoiLluxs5Sk8=
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.4 h1:p+9JllOL5Q2pj6bmP9gu+LdjyRg/XxHLTpMfuhuQsY4=
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.4/go.mod h1:HZxL3asFIkcIJtHdnqsdcXsY6d+1iMtq0SPUlX17TGM=
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.5 h1:nn9or1e5sTDXay/dfsB4E/A4jYaYdPVCXV8mME/maEc=
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.5/go.mod h1:m87GAn4UcesHQF3leaPFEINZETO5za1LGn1GJdNDgNc=
github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20250917110311-16510ac47288 h1:E2tZFeg9mGYGQ7E7BbxMv1cU35HxwgRm6tPKI2Pp7DA=
github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20250917110311-16510ac47288/go.mod h1:WUxgxUDZoCF2sxVmW+STSxatP02Qn3FcafTiI2BLtE0=
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc=
@@ -244,6 +248,8 @@ github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 h1:4chzWmimtJPx
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8=
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a h1:SJy1Pu0eH1C29XwJucQo73FrleVK6t4kYz4NVhp34Yw=
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a/go.mod h1:DFSS3NAGHthKo1gTlmEcSBiZrRJXi28rLNd/1udP1c8=
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 h1:uFsXVBE9Qr4ZoF094vE6iYTLDl0qCiKzYXlL6UeWObU=
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0=
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc h1:24heQPtnFR+yfntqhI3oAu9i27nEojcQ4NuBQOo5ZFA=
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc/go.mod h1:f93CXfllFsO9ZQVq+Zocb1Gp4G5Fz0b0rXHLOzt/Djc=
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 h1:UBPHPtv8+nEAy2PD8RyAhOYvau1ek0HDJqLS/Pysi14=
@@ -262,6 +268,7 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM=
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA=
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
@@ -315,6 +322,8 @@ golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwE
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

View File

@@ -24,6 +24,9 @@ type TailscaleEndpointOptions struct {
AdvertiseExitNode bool `json:"advertise_exit_node,omitempty"`
RelayServerPort *uint16 `json:"relay_server_port,omitempty"`
RelayServerStaticEndpoints []netip.AddrPort `json:"relay_server_static_endpoints,omitempty"`
SystemInterface bool `json:"system_interface,omitempty"`
SystemInterfaceName string `json:"system_interface_name,omitempty"`
SystemInterfaceMTU uint32 `json:"system_interface_mtu,omitempty"`
UDPTimeout UDPTimeoutCompat `json:"udp_timeout,omitempty"`
}

View File

@@ -22,8 +22,6 @@ import (
"github.com/sagernet/gvisor/pkg/tcpip/header"
"github.com/sagernet/gvisor/pkg/tcpip/stack"
"github.com/sagernet/gvisor/pkg/tcpip/transport/icmp"
"github.com/sagernet/gvisor/pkg/tcpip/transport/tcp"
"github.com/sagernet/gvisor/pkg/tcpip/transport/udp"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/adapter/endpoint"
"github.com/sagernet/sing-box/common/dialer"
@@ -49,8 +47,10 @@ import (
tsDNS "github.com/sagernet/tailscale/net/dns"
"github.com/sagernet/tailscale/net/netmon"
"github.com/sagernet/tailscale/net/tsaddr"
tsTUN "github.com/sagernet/tailscale/net/tstun"
"github.com/sagernet/tailscale/tsnet"
"github.com/sagernet/tailscale/types/ipproto"
"github.com/sagernet/tailscale/types/nettype"
"github.com/sagernet/tailscale/version"
"github.com/sagernet/tailscale/wgengine"
"github.com/sagernet/tailscale/wgengine/filter"
@@ -101,6 +101,51 @@ type Endpoint struct {
relayServerStaticEndpoints []netip.AddrPort
udpTimeout time.Duration
systemInterface bool
systemInterfaceName string
systemInterfaceMTU uint32
systemTun tun.Tun
fallbackTCPCloser func()
}
func (t *Endpoint) registerNetstackHandlers() {
netstack := t.server.ExportNetstack()
if netstack == nil {
return
}
previousTCP := netstack.GetTCPHandlerForFlow
netstack.GetTCPHandlerForFlow = func(src, dst netip.AddrPort) (handler func(net.Conn), intercept bool) {
if previousTCP != nil {
handler, intercept = previousTCP(src, dst)
if handler != nil || !intercept {
return handler, intercept
}
}
return func(conn net.Conn) {
ctx := log.ContextWithNewID(t.ctx)
source := M.SocksaddrFrom(src.Addr(), src.Port())
destination := M.SocksaddrFrom(dst.Addr(), dst.Port())
t.NewConnectionEx(ctx, conn, source, destination, nil)
}, true
}
previousUDP := netstack.GetUDPHandlerForFlow
netstack.GetUDPHandlerForFlow = func(src, dst netip.AddrPort) (handler func(nettype.ConnPacketConn), intercept bool) {
if previousUDP != nil {
handler, intercept = previousUDP(src, dst)
if handler != nil || !intercept {
return handler, intercept
}
}
return func(conn nettype.ConnPacketConn) {
ctx := log.ContextWithNewID(t.ctx)
source := M.SocksaddrFrom(src.Addr(), src.Port())
destination := M.SocksaddrFrom(dst.Addr(), dst.Port())
packetConn := bufio.NewPacketConn(conn)
t.NewPacketConnectionEx(ctx, packetConn, source, destination, nil)
}, true
}
}
func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TailscaleEndpointOptions) (adapter.Endpoint, error) {
@@ -202,6 +247,9 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL
relayServerPort: options.RelayServerPort,
relayServerStaticEndpoints: options.RelayServerStaticEndpoints,
udpTimeout: udpTimeout,
systemInterface: options.SystemInterface,
systemInterfaceName: options.SystemInterfaceName,
systemInterfaceMTU: options.SystemInterfaceMTU,
}, nil
}
@@ -237,10 +285,59 @@ func (t *Endpoint) Start(stage adapter.StartStage) error {
setAndroidProtectFunc(t.platformInterface)
}
}
if t.systemInterface {
mtu := t.systemInterfaceMTU
if mtu == 0 {
mtu = uint32(tsTUN.DefaultTUNMTU())
}
tunName := t.systemInterfaceName
if tunName == "" {
tunName = tun.CalculateInterfaceName("tailscale")
}
tunOptions := tun.Options{
Name: tunName,
MTU: mtu,
GSO: true,
InterfaceScope: true,
InterfaceMonitor: t.network.InterfaceMonitor(),
InterfaceFinder: t.network.InterfaceFinder(),
Logger: t.logger,
EXP_ExternalConfiguration: true,
}
systemTun, err := tun.New(tunOptions)
if err != nil {
return err
}
err = systemTun.Start()
if err != nil {
_ = systemTun.Close()
return err
}
wgTunDevice, err := newTunDeviceAdapter(systemTun, int(mtu), t.logger)
if err != nil {
_ = systemTun.Close()
return err
}
t.systemTun = systemTun
t.server.TunDevice = wgTunDevice
}
err := t.server.Start()
if err != nil {
if t.systemTun != nil {
_ = t.systemTun.Close()
}
return err
}
if t.fallbackTCPCloser == nil {
t.fallbackTCPCloser = t.server.RegisterFallbackTCPHandler(func(src, dst netip.AddrPort) (handler func(net.Conn), intercept bool) {
return func(conn net.Conn) {
ctx := log.ContextWithNewID(t.ctx)
source := M.SocksaddrFrom(src.Addr(), src.Port())
destination := M.SocksaddrFrom(dst.Addr(), dst.Port())
t.NewConnectionEx(ctx, conn, source, destination, nil)
}, true
})
}
t.server.ExportLocalBackend().ExportEngine().(wgengine.ExportedUserspaceEngine).SetOnReconfigListener(t.onReconfig)
ipStack := t.server.ExportNetstack().ExportIPStack()
@@ -252,13 +349,12 @@ func (t *Endpoint) Start(stage adapter.StartStage) error {
if gErr != nil {
return gonet.TranslateNetstackError(gErr)
}
ipStack.SetTransportProtocolHandler(tcp.ProtocolNumber, tun.NewTCPForwarder(t.ctx, ipStack, t).HandlePacket)
ipStack.SetTransportProtocolHandler(udp.ProtocolNumber, tun.NewUDPForwarder(t.ctx, ipStack, t, t.udpTimeout).HandlePacket)
icmpForwarder := tun.NewICMPForwarder(t.ctx, ipStack, t, t.udpTimeout)
ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber4, icmpForwarder.HandlePacket)
ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber6, icmpForwarder.HandlePacket)
t.stack = ipStack
t.icmpForwarder = icmpForwarder
t.registerNetstackHandlers()
localBackend := t.server.ExportLocalBackend()
perfs := &ipn.MaskedPrefs{
@@ -355,6 +451,10 @@ func (t *Endpoint) Close() error {
if runtime.GOOS == "android" {
setAndroidProtectFunc(nil)
}
if t.fallbackTCPCloser != nil {
t.fallbackTCPCloser()
t.fallbackTCPCloser = nil
}
return common.Close(common.PtrOrNil(t.server))
}

View File

@@ -0,0 +1,156 @@
//go:build !windows
package tailscale
import (
"encoding/hex"
"errors"
"io"
"os"
"sync"
"sync/atomic"
singTun "github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/logger"
wgTun "github.com/sagernet/wireguard-go/tun"
)
type tunDeviceAdapter struct {
tun singTun.Tun
linuxTUN singTun.LinuxTUN
events chan wgTun.Event
mtu int
logger logger.ContextLogger
debugTun bool
readCount atomic.Uint32
writeCount atomic.Uint32
closeOnce sync.Once
}
func newTunDeviceAdapter(tun singTun.Tun, mtu int, logger logger.ContextLogger) (wgTun.Device, error) {
if tun == nil {
return nil, os.ErrInvalid
}
if mtu == 0 {
mtu = 1500
}
adapter := &tunDeviceAdapter{
tun: tun,
events: make(chan wgTun.Event, 1),
mtu: mtu,
logger: logger,
debugTun: os.Getenv("SINGBOX_TS_TUN_DEBUG") != "",
}
if linuxTUN, ok := tun.(singTun.LinuxTUN); ok {
adapter.linuxTUN = linuxTUN
}
adapter.events <- wgTun.EventUp
return adapter, nil
}
func (a *tunDeviceAdapter) File() *os.File {
return nil
}
func (a *tunDeviceAdapter) Read(bufs [][]byte, sizes []int, offset int) (count int, err error) {
if a.linuxTUN != nil {
n, err := a.linuxTUN.BatchRead(bufs, offset-singTun.PacketOffset, sizes)
if err == nil {
for i := 0; i < n; i++ {
a.debugPacket("read", bufs[i][offset:offset+sizes[i]])
}
}
return n, err
}
if offset < singTun.PacketOffset {
return 0, io.ErrShortBuffer
}
readBuf := bufs[0][offset-singTun.PacketOffset:]
n, err := a.tun.Read(readBuf)
if err == nil {
if n < singTun.PacketOffset {
return 0, io.ErrUnexpectedEOF
}
sizes[0] = n - singTun.PacketOffset
a.debugPacket("read", readBuf[singTun.PacketOffset:n])
return 1, nil
}
if errors.Is(err, singTun.ErrTooManySegments) {
err = wgTun.ErrTooManySegments
}
return 0, err
}
func (a *tunDeviceAdapter) Write(bufs [][]byte, offset int) (count int, err error) {
if a.linuxTUN != nil {
for i := range bufs {
a.debugPacket("write", bufs[i][offset:])
}
return a.linuxTUN.BatchWrite(bufs, offset)
}
for _, packet := range bufs {
a.debugPacket("write", packet[offset:])
if singTun.PacketOffset > 0 {
common.ClearArray(packet[offset-singTun.PacketOffset : offset])
singTun.PacketFillHeader(packet[offset-singTun.PacketOffset:], singTun.PacketIPVersion(packet[offset:]))
}
_, err = a.tun.Write(packet[offset-singTun.PacketOffset:])
if err != nil {
return 0, err
}
}
// WireGuard will not read count.
return 0, nil
}
func (a *tunDeviceAdapter) MTU() (int, error) {
return a.mtu, nil
}
func (a *tunDeviceAdapter) Name() (string, error) {
return a.tun.Name()
}
func (a *tunDeviceAdapter) Events() <-chan wgTun.Event {
return a.events
}
func (a *tunDeviceAdapter) Close() error {
var err error
a.closeOnce.Do(func() {
close(a.events)
err = a.tun.Close()
})
return err
}
func (a *tunDeviceAdapter) BatchSize() int {
if a.linuxTUN != nil {
return a.linuxTUN.BatchSize()
}
return 1
}
func (a *tunDeviceAdapter) debugPacket(direction string, packet []byte) {
if !a.debugTun || a.logger == nil {
return
}
var counter *atomic.Uint32
switch direction {
case "read":
counter = &a.readCount
case "write":
counter = &a.writeCount
default:
return
}
if counter.Add(1) > 8 {
return
}
sample := packet
if len(sample) > 64 {
sample = sample[:64]
}
a.logger.Trace("tailscale tun ", direction, " len=", len(packet), " head=", hex.EncodeToString(sample))
}

View File

@@ -0,0 +1,117 @@
//go:build windows
package tailscale
import (
"errors"
"os"
"sync"
"sync/atomic"
singTun "github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common/logger"
wgTun "github.com/sagernet/wireguard-go/tun"
)
type tunDeviceAdapter struct {
tun singTun.WinTun
nativeTun *singTun.NativeTun
events chan wgTun.Event
mtu atomic.Int64
closeOnce sync.Once
}
func newTunDeviceAdapter(tun singTun.Tun, mtu int, _ logger.ContextLogger) (wgTun.Device, error) {
winTun, ok := tun.(singTun.WinTun)
if !ok {
return nil, errors.New("not a windows tun device")
}
nativeTun, ok := winTun.(*singTun.NativeTun)
if !ok {
return nil, errors.New("unsupported windows tun device")
}
if mtu == 0 {
mtu = 1500
}
adapter := &tunDeviceAdapter{
tun: winTun,
nativeTun: nativeTun,
events: make(chan wgTun.Event, 1),
}
adapter.mtu.Store(int64(mtu))
adapter.events <- wgTun.EventUp
return adapter, nil
}
func (a *tunDeviceAdapter) File() *os.File {
return nil
}
func (a *tunDeviceAdapter) Read(bufs [][]byte, sizes []int, offset int) (count int, err error) {
packet, release, err := a.tun.ReadPacket()
if err != nil {
return 0, err
}
defer release()
sizes[0] = copy(bufs[0][offset-singTun.PacketOffset:], packet)
return 1, nil
}
func (a *tunDeviceAdapter) Write(bufs [][]byte, offset int) (count int, err error) {
for _, packet := range bufs {
if singTun.PacketOffset > 0 {
singTun.PacketFillHeader(packet[offset-singTun.PacketOffset:], singTun.PacketIPVersion(packet[offset:]))
}
_, err = a.tun.Write(packet[offset-singTun.PacketOffset:])
if err != nil {
return 0, err
}
}
return 0, nil
}
func (a *tunDeviceAdapter) MTU() (int, error) {
return int(a.mtu.Load()), nil
}
func (a *tunDeviceAdapter) ForceMTU(mtu int) {
if mtu <= 0 {
return
}
update := int(a.mtu.Load()) != mtu
a.mtu.Store(int64(mtu))
if update {
select {
case a.events <- wgTun.EventMTUUpdate:
default:
}
}
}
func (a *tunDeviceAdapter) LUID() uint64 {
if a.nativeTun == nil {
return 0
}
return a.nativeTun.LUID()
}
func (a *tunDeviceAdapter) Name() (string, error) {
return a.tun.Name()
}
func (a *tunDeviceAdapter) Events() <-chan wgTun.Event {
return a.events
}
func (a *tunDeviceAdapter) Close() error {
var err error
a.closeOnce.Do(func() {
close(a.events)
err = a.tun.Close()
})
return err
}
func (a *tunDeviceAdapter) BatchSize() int {
return 1
}