mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-06-18 17:12:47 +03:00
Compare commits
8 Commits
v1.11.13-e
...
v1.11.14-e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
209b89a4a3 | ||
|
|
765111a552 | ||
|
|
9b8ab3e61e | ||
|
|
47f18e823a | ||
|
|
2d1b824b62 | ||
|
|
d511698f3f | ||
|
|
cb435ea232 | ||
|
|
43a9016c83 |
@@ -42,14 +42,16 @@ type InboundManager interface {
|
||||
}
|
||||
|
||||
type InboundContext struct {
|
||||
Inbound string
|
||||
InboundType string
|
||||
IPVersion uint8
|
||||
Network string
|
||||
Source M.Socksaddr
|
||||
Destination M.Socksaddr
|
||||
User string
|
||||
Outbound string
|
||||
Inbound string
|
||||
InboundType string
|
||||
IPVersion uint8
|
||||
Network string
|
||||
Source M.Socksaddr
|
||||
Destination M.Socksaddr
|
||||
TunnelSource string
|
||||
TunnelDestination string
|
||||
User string
|
||||
Outbound string
|
||||
|
||||
// sniffer
|
||||
|
||||
|
||||
Submodule clients/android updated: 320170a107...eb2e13a6f9
@@ -100,10 +100,6 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
|
||||
} else if networkManager.AutoDetectInterface() {
|
||||
if platformInterface != nil {
|
||||
networkStrategy = (*C.NetworkStrategy)(options.NetworkStrategy)
|
||||
if networkStrategy == nil {
|
||||
networkStrategy = common.Ptr(C.NetworkStrategyDefault)
|
||||
defaultNetworkStrategy = true
|
||||
}
|
||||
networkType = common.Map(options.NetworkType, option.InterfaceType.Build)
|
||||
fallbackNetworkType = common.Map(options.FallbackNetworkType, option.InterfaceType.Build)
|
||||
if networkStrategy == nil && len(networkType) == 0 && len(fallbackNetworkType) == 0 {
|
||||
@@ -115,6 +111,10 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
|
||||
if networkFallbackDelay == 0 && defaultOptions.FallbackDelay != 0 {
|
||||
networkFallbackDelay = defaultOptions.FallbackDelay
|
||||
}
|
||||
if networkStrategy == nil {
|
||||
networkStrategy = common.Ptr(C.NetworkStrategyDefault)
|
||||
defaultNetworkStrategy = true
|
||||
}
|
||||
bindFunc := networkManager.ProtectFunc()
|
||||
dialer.Control = control.Append(dialer.Control, bindFunc)
|
||||
listener.Control = control.Append(listener.Control, bindFunc)
|
||||
|
||||
@@ -10,9 +10,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/bufio"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
|
||||
@@ -26,7 +24,9 @@ type slowOpenConn struct {
|
||||
destination M.Socksaddr
|
||||
conn net.Conn
|
||||
create chan struct{}
|
||||
done chan struct{}
|
||||
access sync.Mutex
|
||||
closeOnce sync.Once
|
||||
err error
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ func DialSlowContext(dialer *tcpDialer, ctx context.Context, network string, des
|
||||
network: network,
|
||||
destination: destination,
|
||||
create: make(chan struct{}),
|
||||
done: make(chan struct{}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -55,8 +56,8 @@ func (c *slowOpenConn) Read(b []byte) (n int, err error) {
|
||||
if c.err != nil {
|
||||
return 0, c.err
|
||||
}
|
||||
case <-c.ctx.Done():
|
||||
return 0, c.ctx.Err()
|
||||
case <-c.done:
|
||||
return 0, os.ErrClosed
|
||||
}
|
||||
}
|
||||
return c.conn.Read(b)
|
||||
@@ -74,12 +75,15 @@ func (c *slowOpenConn) Write(b []byte) (n int, err error) {
|
||||
return 0, c.err
|
||||
}
|
||||
return c.conn.Write(b)
|
||||
case <-c.done:
|
||||
return 0, os.ErrClosed
|
||||
default:
|
||||
}
|
||||
c.conn, err = c.dialer.DialContext(c.ctx, c.network, c.destination.String(), b)
|
||||
conn, err := c.dialer.DialContext(c.ctx, c.network, c.destination.String(), b)
|
||||
if err != nil {
|
||||
c.conn = nil
|
||||
c.err = E.Cause(err, "dial tcp fast open")
|
||||
c.err = err
|
||||
} else {
|
||||
c.conn = conn
|
||||
}
|
||||
n = len(b)
|
||||
close(c.create)
|
||||
@@ -87,7 +91,13 @@ func (c *slowOpenConn) Write(b []byte) (n int, err error) {
|
||||
}
|
||||
|
||||
func (c *slowOpenConn) Close() error {
|
||||
return common.Close(c.conn)
|
||||
c.closeOnce.Do(func() {
|
||||
close(c.done)
|
||||
if c.conn != nil {
|
||||
c.conn.Close()
|
||||
}
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *slowOpenConn) LocalAddr() net.Addr {
|
||||
@@ -152,8 +162,8 @@ func (c *slowOpenConn) WriteTo(w io.Writer) (n int64, err error) {
|
||||
if c.err != nil {
|
||||
return 0, c.err
|
||||
}
|
||||
case <-c.ctx.Done():
|
||||
return 0, c.ctx.Err()
|
||||
case <-c.done:
|
||||
return 0, c.err
|
||||
}
|
||||
}
|
||||
return bufio.Copy(w, c.conn)
|
||||
|
||||
@@ -76,6 +76,8 @@ func findProcessName(network string, ip netip.Addr, port int) (string, error) {
|
||||
// rup8(sizeof(xtcpcb_n))
|
||||
itemSize += 208
|
||||
}
|
||||
|
||||
var fallbackUDPProcess string
|
||||
// skip the first xinpgen(24 bytes) block
|
||||
for i := 24; i+itemSize <= len(buf); i += itemSize {
|
||||
// offset of xinpcb_n and xsocket_n
|
||||
@@ -90,10 +92,12 @@ func findProcessName(network string, ip netip.Addr, port int) (string, error) {
|
||||
flag := buf[inp+44]
|
||||
|
||||
var srcIP netip.Addr
|
||||
srcIsIPv4 := false
|
||||
switch {
|
||||
case flag&0x1 > 0 && isIPv4:
|
||||
// ipv4
|
||||
srcIP = netip.AddrFrom4(*(*[4]byte)(buf[inp+76 : inp+80]))
|
||||
srcIsIPv4 = true
|
||||
case flag&0x2 > 0 && !isIPv4:
|
||||
// ipv6
|
||||
srcIP = netip.AddrFrom16(*(*[16]byte)(buf[inp+64 : inp+80]))
|
||||
@@ -101,13 +105,21 @@ func findProcessName(network string, ip netip.Addr, port int) (string, error) {
|
||||
continue
|
||||
}
|
||||
|
||||
if ip != srcIP {
|
||||
continue
|
||||
if ip == srcIP {
|
||||
// xsocket_n.so_last_pid
|
||||
pid := readNativeUint32(buf[so+68 : so+72])
|
||||
return getExecPathFromPID(pid)
|
||||
}
|
||||
|
||||
// xsocket_n.so_last_pid
|
||||
pid := readNativeUint32(buf[so+68 : so+72])
|
||||
return getExecPathFromPID(pid)
|
||||
// udp packet connection may be not equal with srcIP
|
||||
if network == N.NetworkUDP && srcIP.IsUnspecified() && isIPv4 == srcIsIPv4 {
|
||||
pid := readNativeUint32(buf[so+68 : so+72])
|
||||
fallbackUDPProcess, _ = getExecPathFromPID(pid)
|
||||
}
|
||||
}
|
||||
|
||||
if network == N.NetworkUDP && len(fallbackUDPProcess) > 0 {
|
||||
return fallbackUDPProcess, nil
|
||||
}
|
||||
|
||||
return "", ErrNotFound
|
||||
|
||||
@@ -25,6 +25,8 @@ const (
|
||||
TypeVLESS = "vless"
|
||||
TypeTUIC = "tuic"
|
||||
TypeHysteria2 = "hysteria2"
|
||||
TypeTunnelClient = "tunnel_client"
|
||||
TypeTunnelServer = "tunnel_server"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -86,6 +88,10 @@ func ProxyDisplayName(proxyType string) string {
|
||||
return "Selector"
|
||||
case TypeURLTest:
|
||||
return "URLTest"
|
||||
case TypeTunnelClient:
|
||||
return "Tunnel Client"
|
||||
case TypeTunnelServer:
|
||||
return "Tunnel Server"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
### 1.11.14
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
_We are temporarily unable to update sing-box apps on the App Store because the reviewer mistakenly found that we
|
||||
violated the rules (TestFlight users are not affected)._
|
||||
|
||||
### 1.11.13
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
64
examples/tunnel/client->server/client.json
Normal file
64
examples/tunnel/client->server/client.json
Normal file
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "error"
|
||||
},
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"address": "local",
|
||||
"detour": "direct"
|
||||
}
|
||||
]
|
||||
},
|
||||
"endpoints": [
|
||||
{
|
||||
"type": "tunnel_client",
|
||||
"tag": "tunnel",
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
|
||||
"key": "1c9b2ccf-b0c0-4c26-868d-a55a4edad3fe",
|
||||
"outbound": {
|
||||
"type": "vless",
|
||||
"tag": "vless-out",
|
||||
"server": "0.0.0.0",
|
||||
"server_port": 8000,
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
|
||||
"network": "tcp"
|
||||
}
|
||||
}
|
||||
],
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "mixed",
|
||||
"tag": "mixed-in",
|
||||
"listen_port": 7897
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct-out"
|
||||
},
|
||||
{
|
||||
"type": "dns",
|
||||
"tag": "dns-out"
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"rules": [
|
||||
{
|
||||
"protocol": "dns",
|
||||
"outbound": "dns-out"
|
||||
},
|
||||
{
|
||||
"port": 53,
|
||||
"outbound": "dns-out"
|
||||
},
|
||||
{
|
||||
"outbound": "tunnel",
|
||||
"override_tunnel_destination": "f79f7678-55e7-432d-a15f-6e8ab2b7fe13"
|
||||
}
|
||||
],
|
||||
"final": "direct-out",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
}
|
||||
62
examples/tunnel/client->server/server.json
Normal file
62
examples/tunnel/client->server/server.json
Normal file
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "error"
|
||||
},
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"address": "local",
|
||||
"detour": "direct"
|
||||
}
|
||||
]
|
||||
},
|
||||
"endpoints": [
|
||||
{
|
||||
"type": "tunnel_server",
|
||||
"tag": "tunnel",
|
||||
"uuid": "f79f7678-55e7-432d-a15f-6e8ab2b7fe13",
|
||||
"users": [
|
||||
{
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
|
||||
"key": "1c9b2ccf-b0c0-4c26-868d-a55a4edad3fe"
|
||||
}
|
||||
],
|
||||
"inbound": {
|
||||
"type": "vless",
|
||||
"tag": "vless-in",
|
||||
"listen": "0.0.0.0",
|
||||
"listen_port": 8000,
|
||||
"users": [
|
||||
{
|
||||
"name": "vless",
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct-out"
|
||||
},
|
||||
{
|
||||
"type": "dns",
|
||||
"tag": "dns-out"
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"rules": [
|
||||
{
|
||||
"protocol": "dns",
|
||||
"outbound": "dns-out"
|
||||
},
|
||||
{
|
||||
"port": 53,
|
||||
"outbound": "dns-out"
|
||||
}
|
||||
],
|
||||
"final": "direct-out",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
}
|
||||
64
examples/tunnel/client1->server->client2/client1.json
Normal file
64
examples/tunnel/client1->server->client2/client1.json
Normal file
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "error"
|
||||
},
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"address": "local",
|
||||
"detour": "direct"
|
||||
}
|
||||
]
|
||||
},
|
||||
"endpoints": [
|
||||
{
|
||||
"type": "tunnel_client",
|
||||
"tag": "tunnel",
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
|
||||
"key": "1c9b2ccf-b0c0-4c26-868d-a55a4edad3fe",
|
||||
"outbound": {
|
||||
"type": "vless",
|
||||
"tag": "vless-out",
|
||||
"server": "0.0.0.0",
|
||||
"server_port": 8000,
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
|
||||
"network": "tcp"
|
||||
}
|
||||
}
|
||||
],
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "mixed",
|
||||
"tag": "mixed-in",
|
||||
"listen_port": 7897
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct-out"
|
||||
},
|
||||
{
|
||||
"type": "dns",
|
||||
"tag": "dns-out"
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"rules": [
|
||||
{
|
||||
"protocol": "dns",
|
||||
"outbound": "dns-out"
|
||||
},
|
||||
{
|
||||
"port": 53,
|
||||
"outbound": "dns-out"
|
||||
},
|
||||
{
|
||||
"outbound": "tunnel",
|
||||
"override_tunnel_destination": "487f6073-3300-4819-a07d-39652e45fb4d"
|
||||
}
|
||||
],
|
||||
"final": "direct-out",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
}
|
||||
53
examples/tunnel/client1->server->client2/client2.json
Normal file
53
examples/tunnel/client1->server->client2/client2.json
Normal file
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "error"
|
||||
},
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"address": "local",
|
||||
"detour": "direct"
|
||||
}
|
||||
]
|
||||
},
|
||||
"endpoints": [
|
||||
{
|
||||
"type": "tunnel_client",
|
||||
"tag": "tunnel",
|
||||
"uuid": "487f6073-3300-4819-a07d-39652e45fb4d",
|
||||
"key": "3d74d616-2502-4c17-9cc3-92c366550f4f",
|
||||
"outbound": {
|
||||
"type": "vless",
|
||||
"tag": "vless-out",
|
||||
"server": "0.0.0.0",
|
||||
"server_port": 8000,
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
|
||||
"network": "tcp"
|
||||
}
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct-out"
|
||||
},
|
||||
{
|
||||
"type": "dns",
|
||||
"tag": "dns-out"
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"rules": [
|
||||
{
|
||||
"protocol": "dns",
|
||||
"outbound": "dns-out"
|
||||
},
|
||||
{
|
||||
"port": 53,
|
||||
"outbound": "dns-out"
|
||||
}
|
||||
],
|
||||
"final": "direct-out",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
}
|
||||
77
examples/tunnel/client1->server->client2/server.json
Normal file
77
examples/tunnel/client1->server->client2/server.json
Normal file
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "error"
|
||||
},
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"address": "local",
|
||||
"detour": "direct"
|
||||
}
|
||||
]
|
||||
},
|
||||
"endpoints": [
|
||||
{
|
||||
"type": "tunnel_server",
|
||||
"tag": "tunnel",
|
||||
"uuid": "f79f7678-55e7-432d-a15f-6e8ab2b7fe13",
|
||||
"users": [
|
||||
{
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
|
||||
"key": "1c9b2ccf-b0c0-4c26-868d-a55a4edad3fe"
|
||||
},
|
||||
{
|
||||
"uuid": "487f6073-3300-4819-a07d-39652e45fb4d",
|
||||
"key": "3d74d616-2502-4c17-9cc3-92c366550f4f"
|
||||
}
|
||||
],
|
||||
"inbound": {
|
||||
"type": "vless",
|
||||
"tag": "vless-in",
|
||||
"listen": "0.0.0.0",
|
||||
"listen_port": 8000,
|
||||
"users": [
|
||||
{
|
||||
"name": "vless",
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct-out"
|
||||
},
|
||||
{
|
||||
"type": "dns",
|
||||
"tag": "dns-out"
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"rules": [
|
||||
{
|
||||
"protocol": "dns",
|
||||
"outbound": "dns-out"
|
||||
},
|
||||
{
|
||||
"port": 53,
|
||||
"outbound": "dns-out"
|
||||
},
|
||||
{
|
||||
"tunnel_source": [
|
||||
"9b65b7e1-04c8-4717-8f45-2aa61fd25937",
|
||||
"487f6073-3300-4819-a07d-39652e45fb4d"
|
||||
],
|
||||
"tunnel_destination": [
|
||||
"9b65b7e1-04c8-4717-8f45-2aa61fd25937",
|
||||
"487f6073-3300-4819-a07d-39652e45fb4d"
|
||||
],
|
||||
"outbound": "tunnel"
|
||||
}
|
||||
],
|
||||
"final": "direct-out",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "error"
|
||||
},
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"address": "local",
|
||||
"detour": "direct"
|
||||
}
|
||||
]
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "mixed",
|
||||
"tag": "mixed-in",
|
||||
"listen_port": 7897
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct-out"
|
||||
},
|
||||
{
|
||||
"type": "vless",
|
||||
"tag": "vless-out",
|
||||
"server": "0.0.0.0",
|
||||
"server_port": 8000,
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
|
||||
"network": "tcp"
|
||||
},
|
||||
{
|
||||
"type": "dns",
|
||||
"tag": "dns-out"
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"rules": [
|
||||
{
|
||||
"protocol": "dns",
|
||||
"outbound": "dns-out"
|
||||
},
|
||||
{
|
||||
"port": 53,
|
||||
"outbound": "dns-out"
|
||||
}
|
||||
],
|
||||
"final": "vless-out",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "error"
|
||||
},
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"address": "local",
|
||||
"detour": "direct"
|
||||
}
|
||||
]
|
||||
},
|
||||
"endpoints": [
|
||||
{
|
||||
"type": "tunnel_server",
|
||||
"tag": "tunnel",
|
||||
"uuid": "f79f7678-55e7-432d-a15f-6e8ab2b7fe13",
|
||||
"users": [
|
||||
{
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
|
||||
"key": "1c9b2ccf-b0c0-4c26-868d-a55a4edad3fe"
|
||||
}
|
||||
],
|
||||
"inbound": {
|
||||
"type": "vless",
|
||||
"tag": "vless-in",
|
||||
"listen": "0.0.0.0",
|
||||
"listen_port": 8000,
|
||||
"users": [
|
||||
{
|
||||
"name": "vless",
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct-out"
|
||||
},
|
||||
{
|
||||
"type": "dns",
|
||||
"tag": "dns-out"
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"rules": [
|
||||
{
|
||||
"protocol": "dns",
|
||||
"outbound": "dns-out"
|
||||
},
|
||||
{
|
||||
"port": 53,
|
||||
"outbound": "dns-out"
|
||||
},
|
||||
{
|
||||
"inbound": "vless-in",
|
||||
"outbound": "tunnel",
|
||||
"override_tunnel_destination": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
|
||||
}
|
||||
],
|
||||
"final": "direct-out",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "error"
|
||||
},
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"address": "local",
|
||||
"detour": "direct"
|
||||
}
|
||||
]
|
||||
},
|
||||
"endpoints": [
|
||||
{
|
||||
"type": "tunnel_client",
|
||||
"tag": "tunnel",
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
|
||||
"key": "1c9b2ccf-b0c0-4c26-868d-a55a4edad3fe",
|
||||
"outbound": {
|
||||
"type": "vless",
|
||||
"tag": "vless-out",
|
||||
"server": "0.0.0.0",
|
||||
"server_port": 8000,
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
|
||||
"network": "tcp"
|
||||
}
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct-out"
|
||||
},
|
||||
{
|
||||
"type": "dns",
|
||||
"tag": "dns-out"
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"rules": [
|
||||
{
|
||||
"protocol": "dns",
|
||||
"outbound": "dns-out"
|
||||
},
|
||||
{
|
||||
"port": 53,
|
||||
"outbound": "dns-out"
|
||||
}
|
||||
],
|
||||
"final": "direct-out",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
}
|
||||
53
examples/tunnel/server->client/client.json
Normal file
53
examples/tunnel/server->client/client.json
Normal file
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "error"
|
||||
},
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"address": "local",
|
||||
"detour": "direct"
|
||||
}
|
||||
]
|
||||
},
|
||||
"endpoints": [
|
||||
{
|
||||
"type": "tunnel_client",
|
||||
"tag": "tunnel",
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
|
||||
"key": "1c9b2ccf-b0c0-4c26-868d-a55a4edad3fe",
|
||||
"outbound": {
|
||||
"type": "vless",
|
||||
"tag": "vless-out",
|
||||
"server": "0.0.0.0",
|
||||
"server_port": 8000,
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
|
||||
"network": "tcp"
|
||||
}
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct-out"
|
||||
},
|
||||
{
|
||||
"type": "dns",
|
||||
"tag": "dns-out"
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"rules": [
|
||||
{
|
||||
"protocol": "dns",
|
||||
"outbound": "dns-out"
|
||||
},
|
||||
{
|
||||
"port": 53,
|
||||
"outbound": "dns-out"
|
||||
}
|
||||
],
|
||||
"final": "direct-out",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
}
|
||||
73
examples/tunnel/server->client/server.json
Normal file
73
examples/tunnel/server->client/server.json
Normal file
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "error"
|
||||
},
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"address": "local",
|
||||
"detour": "direct"
|
||||
}
|
||||
]
|
||||
},
|
||||
"endpoints": [
|
||||
{
|
||||
"type": "tunnel_server",
|
||||
"tag": "tunnel",
|
||||
"uuid": "f79f7678-55e7-432d-a15f-6e8ab2b7fe13",
|
||||
"users": [
|
||||
{
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
|
||||
"key": "1c9b2ccf-b0c0-4c26-868d-a55a4edad3fe"
|
||||
}
|
||||
],
|
||||
"inbound": {
|
||||
"type": "vless",
|
||||
"tag": "vless-in",
|
||||
"listen": "0.0.0.0",
|
||||
"listen_port": 8000,
|
||||
"users": [
|
||||
{
|
||||
"name": "vless",
|
||||
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "mixed",
|
||||
"tag": "mixed-in",
|
||||
"listen_port": 7897
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct-out"
|
||||
},
|
||||
{
|
||||
"type": "dns",
|
||||
"tag": "dns-out"
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"rules": [
|
||||
{
|
||||
"protocol": "dns",
|
||||
"outbound": "dns-out"
|
||||
},
|
||||
{
|
||||
"port": 53,
|
||||
"outbound": "dns-out"
|
||||
},
|
||||
{
|
||||
"outbound": "tunnel",
|
||||
"override_tunnel_destination": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
|
||||
}
|
||||
],
|
||||
"final": "direct-out",
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
}
|
||||
2
go.mod
2
go.mod
@@ -37,7 +37,7 @@ require (
|
||||
github.com/sagernet/sing-shadowsocks v0.2.8
|
||||
github.com/sagernet/sing-shadowsocks2 v0.2.1
|
||||
github.com/sagernet/sing-shadowtls v0.2.0
|
||||
github.com/sagernet/sing-tun v0.6.5
|
||||
github.com/sagernet/sing-tun v0.6.8
|
||||
github.com/sagernet/sing-vmess v0.2.3
|
||||
github.com/sagernet/smux v1.5.34-mod.2
|
||||
github.com/sagernet/utls v1.6.7
|
||||
|
||||
4
go.sum
4
go.sum
@@ -149,8 +149,8 @@ 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.0 h1:cLKe4OAOFwuhmAIuPLj//CIL7Q9js+pIDardhJ+/osk=
|
||||
github.com/sagernet/sing-shadowtls v0.2.0/go.mod h1:agU+Fw5X+xnWVyRHyFthoZCX3MfWKCFPm4JUf+1oaxo=
|
||||
github.com/sagernet/sing-tun v0.6.5 h1:nGfD6GNq/r0tEjdZHOV3BS6fydSmd4kBAokU5rffssg=
|
||||
github.com/sagernet/sing-tun v0.6.5/go.mod h1:fisFCbC4Vfb6HqQNcwPJi2CDK2bf0Xapyz3j3t4cnHE=
|
||||
github.com/sagernet/sing-tun v0.6.8 h1:tr+LKHe09C2I9GfNuB2vnzaZm+ekoNlAhLLrdiLjtAA=
|
||||
github.com/sagernet/sing-tun v0.6.8/go.mod h1:fisFCbC4Vfb6HqQNcwPJi2CDK2bf0Xapyz3j3t4cnHE=
|
||||
github.com/sagernet/sing-vmess v0.2.3 h1:z6Ym8dnZG7k1fP3+54vz8G0tvRVJeOoTFFeUPwXTD44=
|
||||
github.com/sagernet/sing-vmess v0.2.3/go.mod h1:jDAZ0A0St1zVRkyvhAPRySOFfhC+4SQtO5VYyeFotgA=
|
||||
github.com/sagernet/smux v1.5.34-mod.2 h1:gkmBjIjlJ2zQKpLigOkFur5kBKdV6bNRoFu2WkltRQ4=
|
||||
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
"github.com/sagernet/sing-box/protocol/tor"
|
||||
"github.com/sagernet/sing-box/protocol/trojan"
|
||||
"github.com/sagernet/sing-box/protocol/tun"
|
||||
"github.com/sagernet/sing-box/protocol/tunnel"
|
||||
"github.com/sagernet/sing-box/protocol/vless"
|
||||
"github.com/sagernet/sing-box/protocol/vmess"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
@@ -88,6 +89,9 @@ func OutboundRegistry() *outbound.Registry {
|
||||
func EndpointRegistry() *endpoint.Registry {
|
||||
registry := endpoint.NewRegistry()
|
||||
|
||||
tunnel.RegisterServerEndpoint(registry)
|
||||
tunnel.RegisterClientEndpoint(registry)
|
||||
|
||||
registerWireGuardEndpoint(registry)
|
||||
|
||||
return registry
|
||||
|
||||
@@ -88,6 +88,8 @@ type RawDefaultRule struct {
|
||||
SourcePortRange badoption.Listable[string] `json:"source_port_range,omitempty"`
|
||||
Port badoption.Listable[uint16] `json:"port,omitempty"`
|
||||
PortRange badoption.Listable[string] `json:"port_range,omitempty"`
|
||||
TunnelSource badoption.Listable[string] `json:"tunnel_source,omitempty"`
|
||||
TunnelDestination badoption.Listable[string] `json:"tunnel_destination,omitempty"`
|
||||
ProcessName badoption.Listable[string] `json:"process_name,omitempty"`
|
||||
ProcessPath badoption.Listable[string] `json:"process_path,omitempty"`
|
||||
ProcessPathRegex badoption.Listable[string] `json:"process_path_regex,omitempty"`
|
||||
|
||||
@@ -142,8 +142,9 @@ type RouteActionOptions struct {
|
||||
}
|
||||
|
||||
type RawRouteOptionsActionOptions struct {
|
||||
OverrideAddress string `json:"override_address,omitempty"`
|
||||
OverridePort uint16 `json:"override_port,omitempty"`
|
||||
OverrideAddress string `json:"override_address,omitempty"`
|
||||
OverridePort uint16 `json:"override_port,omitempty"`
|
||||
OverrideTunnelDestination string `json:"override_tunnel_destination,omitempty"`
|
||||
|
||||
NetworkStrategy *NetworkStrategy `json:"network_strategy,omitempty"`
|
||||
FallbackDelay uint32 `json:"fallback_delay,omitempty"`
|
||||
|
||||
@@ -89,6 +89,8 @@ type RawDefaultDNSRule struct {
|
||||
SourcePortRange badoption.Listable[string] `json:"source_port_range,omitempty"`
|
||||
Port badoption.Listable[uint16] `json:"port,omitempty"`
|
||||
PortRange badoption.Listable[string] `json:"port_range,omitempty"`
|
||||
TunnelSource badoption.Listable[string] `json:"tunnel_source,omitempty"`
|
||||
TunnelDestination badoption.Listable[string] `json:"tunnel_destination,omitempty"`
|
||||
ProcessName badoption.Listable[string] `json:"process_name,omitempty"`
|
||||
ProcessPath badoption.Listable[string] `json:"process_path,omitempty"`
|
||||
ProcessPathRegex badoption.Listable[string] `json:"process_path_regex,omitempty"`
|
||||
|
||||
@@ -194,6 +194,8 @@ type DefaultHeadlessRule struct {
|
||||
SourcePortRange badoption.Listable[string] `json:"source_port_range,omitempty"`
|
||||
Port badoption.Listable[uint16] `json:"port,omitempty"`
|
||||
PortRange badoption.Listable[string] `json:"port_range,omitempty"`
|
||||
TunnelSource badoption.Listable[string] `json:"tunnel_source,omitempty"`
|
||||
TunnelDestination badoption.Listable[string] `json:"tunnel_destination,omitempty"`
|
||||
ProcessName badoption.Listable[string] `json:"process_name,omitempty"`
|
||||
ProcessPath badoption.Listable[string] `json:"process_path,omitempty"`
|
||||
ProcessPathRegex badoption.Listable[string] `json:"process_path_regex,omitempty"`
|
||||
|
||||
21
option/tunnel.go
Normal file
21
option/tunnel.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package option
|
||||
|
||||
import "github.com/sagernet/sing/common/json/badoption"
|
||||
|
||||
type TunnelClientEndpointOptions struct {
|
||||
UUID string `json:"uuid"`
|
||||
Key string `json:"key"`
|
||||
Outbound Outbound `json:"outbound"`
|
||||
}
|
||||
|
||||
type TunnelServerEndpointOptions struct {
|
||||
UUID string `json:"uuid"`
|
||||
Users []TunnelUser `json:"users"`
|
||||
Inbound Inbound `json:"inbound"`
|
||||
ConnectTimeout badoption.Duration `json:"connect_timeout,omitempty"`
|
||||
}
|
||||
|
||||
type TunnelUser struct {
|
||||
UUID string `json:"uuid"`
|
||||
Key string `json:"key"`
|
||||
}
|
||||
151
protocol/tunnel/client.go
Normal file
151
protocol/tunnel/client.go
Normal file
@@ -0,0 +1,151 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/gofrs/uuid/v5"
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/adapter/endpoint"
|
||||
"github.com/sagernet/sing-box/adapter/outbound"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/service"
|
||||
)
|
||||
|
||||
func RegisterClientEndpoint(registry *endpoint.Registry) {
|
||||
endpoint.Register[option.TunnelClientEndpointOptions](registry, C.TypeTunnelClient, NewClientEndpoint)
|
||||
}
|
||||
|
||||
type ClientEndpoint struct {
|
||||
outbound.Adapter
|
||||
ctx context.Context
|
||||
outbound adapter.Outbound
|
||||
router adapter.ConnectionRouterEx
|
||||
logger logger.ContextLogger
|
||||
uuid uuid.UUID
|
||||
key uuid.UUID
|
||||
}
|
||||
|
||||
func NewClientEndpoint(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TunnelClientEndpointOptions) (adapter.Endpoint, error) {
|
||||
clientUUID, err := uuid.FromString(options.UUID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
clientKey, err := uuid.FromString(options.Key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client := &ClientEndpoint{
|
||||
Adapter: outbound.NewAdapter(C.TypeTunnelClient, tag, []string{N.NetworkTCP}, []string{}),
|
||||
ctx: ctx,
|
||||
router: router,
|
||||
logger: logger,
|
||||
uuid: clientUUID,
|
||||
key: clientKey,
|
||||
}
|
||||
outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx)
|
||||
outbound, err := outboundRegistry.CreateOutbound(ctx, router, logger, options.Outbound.Tag, options.Outbound.Type, options.Outbound.Options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client.outbound = outbound
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (c *ClientEndpoint) Start(stage adapter.StartStage) error {
|
||||
if stage != adapter.StartStatePostStart {
|
||||
return nil
|
||||
}
|
||||
for range 5 {
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-c.ctx.Done():
|
||||
return
|
||||
default:
|
||||
err := c.startInboundConn()
|
||||
if err != nil {
|
||||
c.logger.ErrorContext(c.ctx, err)
|
||||
time.Sleep(time.Second * 5)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ClientEndpoint) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||
if network != N.NetworkTCP {
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
var destinationUUID *uuid.UUID
|
||||
if metadata := adapter.ContextFrom(ctx); metadata != nil {
|
||||
if metadata.TunnelDestination != "" {
|
||||
uuid, err := uuid.FromString(metadata.TunnelDestination)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
destinationUUID = &uuid
|
||||
}
|
||||
}
|
||||
if destinationUUID == nil {
|
||||
return nil, E.New("tunnel destination not set")
|
||||
}
|
||||
if *destinationUUID == c.uuid {
|
||||
return nil, E.New("routing loop")
|
||||
}
|
||||
conn, err := c.outbound.DialContext(ctx, N.NetworkTCP, Destination)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = WriteRequest(conn, &Request{UUID: c.key, Command: CommandTCP, DestinationUUID: *destinationUUID, Destination: destination})
|
||||
return conn, err
|
||||
}
|
||||
|
||||
func (c *ClientEndpoint) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
|
||||
func (c *ClientEndpoint) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ClientEndpoint) startInboundConn() error {
|
||||
conn, err := c.outbound.DialContext(c.ctx, N.NetworkTCP, Destination)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = WriteRequest(conn, &Request{UUID: c.key, Command: CommandInbound, Destination: Destination})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
request, err := ReadRequest(conn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go c.connHandler(conn, request)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ClientEndpoint) connHandler(conn net.Conn, request *Request) {
|
||||
metadata := adapter.InboundContext{
|
||||
Source: M.ParseSocksaddr(conn.RemoteAddr().String()),
|
||||
Destination: request.Destination,
|
||||
}
|
||||
if request.UUID == c.uuid {
|
||||
c.logger.ErrorContext(c.ctx, "routing loop")
|
||||
conn.Close()
|
||||
return
|
||||
}
|
||||
metadata.TunnelSource = request.UUID.String()
|
||||
c.router.RouteConnectionEx(c.ctx, conn, metadata, func(it error) {})
|
||||
}
|
||||
91
protocol/tunnel/protocol.go
Normal file
91
protocol/tunnel/protocol.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"io"
|
||||
|
||||
"github.com/gofrs/uuid/v5"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
)
|
||||
|
||||
const (
|
||||
Version = 0
|
||||
)
|
||||
|
||||
const (
|
||||
CommandInbound = 1
|
||||
CommandTCP = 2
|
||||
)
|
||||
|
||||
var Destination = M.Socksaddr{
|
||||
Fqdn: "sp.tunnel.sing-box.arpa",
|
||||
Port: 444,
|
||||
}
|
||||
|
||||
var AddressSerializer = M.NewSerializer(
|
||||
M.AddressFamilyByte(0x01, M.AddressFamilyIPv4),
|
||||
M.AddressFamilyByte(0x03, M.AddressFamilyIPv6),
|
||||
M.AddressFamilyByte(0x02, M.AddressFamilyFqdn),
|
||||
M.PortThenAddress(),
|
||||
)
|
||||
|
||||
type Request struct {
|
||||
UUID uuid.UUID
|
||||
Command byte
|
||||
DestinationUUID uuid.UUID
|
||||
Destination M.Socksaddr
|
||||
}
|
||||
|
||||
func ReadRequest(reader io.Reader) (*Request, error) {
|
||||
var request Request
|
||||
var version uint8
|
||||
err := binary.Read(reader, binary.BigEndian, &version)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if version != Version {
|
||||
return nil, E.New("unknown version: ", version)
|
||||
}
|
||||
_, err = io.ReadFull(reader, request.UUID[:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = binary.Read(reader, binary.BigEndian, &request.Command)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = io.ReadFull(reader, request.DestinationUUID[:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
request.Destination, err = AddressSerializer.ReadAddrPort(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &request, nil
|
||||
}
|
||||
|
||||
func WriteRequest(writer io.Writer, request *Request) error {
|
||||
var requestLen int
|
||||
requestLen += 1 // version
|
||||
requestLen += 16 // UUID
|
||||
requestLen += 16 // destinationUUID
|
||||
requestLen += 1 // command
|
||||
requestLen += AddressSerializer.AddrPortLen(request.Destination)
|
||||
buffer := buf.NewSize(requestLen)
|
||||
defer buffer.Release()
|
||||
common.Must(
|
||||
buffer.WriteByte(Version),
|
||||
common.Error(buffer.Write(request.UUID[:])),
|
||||
buffer.WriteByte(request.Command),
|
||||
common.Error(buffer.Write(request.DestinationUUID[:])),
|
||||
)
|
||||
err := AddressSerializer.WriteAddrPort(buffer, request.Destination)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return common.Error(writer.Write(buffer.Bytes()))
|
||||
}
|
||||
41
protocol/tunnel/router.go
Normal file
41
protocol/tunnel/router.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
type Router struct {
|
||||
adapter.Router
|
||||
logger logger.ContextLogger
|
||||
handler func(context.Context, net.Conn, adapter.InboundContext, N.CloseHandlerFunc) error
|
||||
}
|
||||
|
||||
func NewRouter(router adapter.Router, logger logger.ContextLogger, handler func(context.Context, net.Conn, adapter.InboundContext, N.CloseHandlerFunc) error) *Router {
|
||||
return &Router{Router: router, logger: logger, handler: handler}
|
||||
}
|
||||
|
||||
func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
|
||||
return r.handler(ctx, conn, metadata, func(error) {})
|
||||
}
|
||||
|
||||
func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
|
||||
func (r *Router) RouteConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
|
||||
if err := r.handler(ctx, conn, metadata, onClose); err != nil {
|
||||
r.logger.ErrorContext(ctx, err)
|
||||
N.CloseOnHandshakeFailure(conn, onClose, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Router) RoutePacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
|
||||
r.logger.ErrorContext(ctx, os.ErrInvalid)
|
||||
N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid)
|
||||
}
|
||||
203
protocol/tunnel/server.go
Normal file
203
protocol/tunnel/server.go
Normal file
@@ -0,0 +1,203 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gofrs/uuid/v5"
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/adapter/endpoint"
|
||||
"github.com/sagernet/sing-box/adapter/outbound"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/service"
|
||||
)
|
||||
|
||||
func RegisterServerEndpoint(registry *endpoint.Registry) {
|
||||
endpoint.Register[option.TunnelServerEndpointOptions](registry, C.TypeTunnelServer, NewServerEndpoint)
|
||||
}
|
||||
|
||||
type ServerEndpoint struct {
|
||||
outbound.Adapter
|
||||
logger logger.ContextLogger
|
||||
inbound adapter.Inbound
|
||||
router adapter.Router
|
||||
uuid uuid.UUID
|
||||
users map[uuid.UUID]uuid.UUID
|
||||
keys map[uuid.UUID]uuid.UUID
|
||||
conns map[uuid.UUID]chan net.Conn
|
||||
timeout time.Duration
|
||||
|
||||
mtx sync.Mutex
|
||||
}
|
||||
|
||||
func NewServerEndpoint(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TunnelServerEndpointOptions) (adapter.Endpoint, error) {
|
||||
serverUUID, err := uuid.FromString(options.UUID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
server := &ServerEndpoint{
|
||||
Adapter: outbound.NewAdapter(C.TypeTunnelServer, tag, []string{N.NetworkTCP}, []string{}),
|
||||
logger: logger,
|
||||
router: router,
|
||||
uuid: serverUUID,
|
||||
}
|
||||
inboundRegistry := service.FromContext[adapter.InboundRegistry](ctx)
|
||||
inbound, err := inboundRegistry.Create(ctx, NewRouter(router, logger, server.connHandler), logger, options.Inbound.Tag, options.Inbound.Type, options.Inbound.Options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
server.inbound = inbound
|
||||
server.users = make(map[uuid.UUID]uuid.UUID, len(options.Users))
|
||||
server.keys = make(map[uuid.UUID]uuid.UUID, len(options.Users))
|
||||
server.conns = make(map[uuid.UUID]chan net.Conn)
|
||||
for _, user := range options.Users {
|
||||
key, err := uuid.FromString(user.Key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uuid, err := uuid.FromString(user.UUID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
server.users[key] = uuid
|
||||
server.keys[uuid] = key
|
||||
server.conns[uuid] = make(chan net.Conn, 10)
|
||||
}
|
||||
if options.ConnectTimeout != 0 {
|
||||
server.timeout = time.Duration(options.ConnectTimeout)
|
||||
} else {
|
||||
server.timeout = C.TCPConnectTimeout
|
||||
}
|
||||
return server, nil
|
||||
}
|
||||
|
||||
func (s *ServerEndpoint) Start(stage adapter.StartStage) error {
|
||||
return s.inbound.Start(stage)
|
||||
}
|
||||
|
||||
func (s *ServerEndpoint) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||
if network != N.NetworkTCP {
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
var sourceUUID *uuid.UUID
|
||||
var ch chan net.Conn
|
||||
if metadata := adapter.ContextFrom(ctx); metadata != nil {
|
||||
if metadata.TunnelDestination != "" {
|
||||
tunnelDestination, err := uuid.FromString(metadata.TunnelDestination)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.mtx.Lock()
|
||||
var ok bool
|
||||
ch, ok = s.conns[tunnelDestination]
|
||||
if !ok {
|
||||
return nil, E.New("user ", metadata.TunnelDestination, " not found")
|
||||
}
|
||||
s.mtx.Unlock()
|
||||
}
|
||||
if metadata.TunnelSource != "" {
|
||||
tunnelSource, err := uuid.FromString(metadata.TunnelSource)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sourceUUID = &tunnelSource
|
||||
}
|
||||
}
|
||||
if ch == nil {
|
||||
return nil, E.New("tunnel destination not set")
|
||||
}
|
||||
if sourceUUID == nil {
|
||||
sourceUUID = &s.uuid
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(ctx, s.timeout)
|
||||
defer cancel()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
default:
|
||||
}
|
||||
select {
|
||||
case conn := <-ch:
|
||||
err := WriteRequest(conn, &Request{UUID: *sourceUUID, Command: CommandTCP, Destination: destination})
|
||||
if err != nil {
|
||||
s.logger.ErrorContext(ctx, err)
|
||||
continue
|
||||
}
|
||||
return conn, nil
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ServerEndpoint) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
|
||||
func (s *ServerEndpoint) Close() error {
|
||||
return common.Close(s.inbound)
|
||||
}
|
||||
|
||||
func (s *ServerEndpoint) connHandler(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) error {
|
||||
if metadata.Destination != Destination {
|
||||
s.router.RouteConnectionEx(ctx, conn, metadata, onClose)
|
||||
return nil
|
||||
}
|
||||
request, err := ReadRequest(conn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if request.Command == CommandInbound {
|
||||
s.mtx.Lock()
|
||||
defer s.mtx.Unlock()
|
||||
uuid, ok := s.users[request.UUID]
|
||||
if !ok {
|
||||
return E.New("key ", request.UUID.String(), " not found")
|
||||
}
|
||||
ch := s.conns[uuid]
|
||||
select {
|
||||
case ch <- conn:
|
||||
default:
|
||||
oldConn := <-ch
|
||||
oldConn.Close()
|
||||
ch <- conn
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if request.Command == CommandTCP {
|
||||
sourceUUID, ok := s.users[request.UUID]
|
||||
if !ok {
|
||||
return E.New("key ", request.UUID, " not found")
|
||||
}
|
||||
if sourceUUID == request.DestinationUUID {
|
||||
return E.New("routing loop on ", sourceUUID)
|
||||
}
|
||||
s.mtx.Lock()
|
||||
if request.DestinationUUID != s.uuid {
|
||||
_, ok = s.keys[request.DestinationUUID]
|
||||
if !ok {
|
||||
return E.New("user ", sourceUUID, " not found")
|
||||
}
|
||||
}
|
||||
s.mtx.Unlock()
|
||||
metadata.Inbound = s.Tag()
|
||||
metadata.InboundType = C.TypeTunnelServer
|
||||
metadata.Destination = request.Destination
|
||||
metadata.TunnelSource = sourceUUID.String()
|
||||
metadata.TunnelDestination = request.DestinationUUID.String()
|
||||
s.router.RouteConnectionEx(ctx, conn, metadata, onClose)
|
||||
return nil
|
||||
}
|
||||
return E.New("command ", request.Command, " not found")
|
||||
}
|
||||
12
route/dns.go
12
route/dns.go
@@ -31,7 +31,7 @@ func (r *Router) hijackDNSStream(ctx context.Context, conn net.Conn, metadata ad
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Router) hijackDNSPacket(ctx context.Context, conn N.PacketConn, packetBuffers []*N.PacketBuffer, metadata adapter.InboundContext) {
|
||||
func (r *Router) hijackDNSPacket(ctx context.Context, conn N.PacketConn, packetBuffers []*N.PacketBuffer, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
|
||||
if natConn, isNatConn := conn.(udpnat.Conn); isNatConn {
|
||||
metadata.Destination = M.Socksaddr{}
|
||||
for _, packet := range packetBuffers {
|
||||
@@ -45,10 +45,12 @@ func (r *Router) hijackDNSPacket(ctx context.Context, conn N.PacketConn, packetB
|
||||
conn: conn,
|
||||
ctx: ctx,
|
||||
metadata: metadata,
|
||||
onClose: onClose,
|
||||
})
|
||||
return
|
||||
}
|
||||
err := dnsOutbound.NewDNSPacketConnection(ctx, r, conn, packetBuffers, metadata)
|
||||
N.CloseOnHandshakeFailure(conn, onClose, err)
|
||||
if err != nil && !E.IsClosedOrCanceled(err) {
|
||||
r.dnsLogger.ErrorContext(ctx, E.Cause(err, "process packet connection"))
|
||||
}
|
||||
@@ -85,8 +87,16 @@ type dnsHijacker struct {
|
||||
conn N.PacketConn
|
||||
ctx context.Context
|
||||
metadata adapter.InboundContext
|
||||
onClose N.CloseHandlerFunc
|
||||
}
|
||||
|
||||
func (h *dnsHijacker) NewPacketEx(buffer *buf.Buffer, destination M.Socksaddr) {
|
||||
go ExchangeDNSPacket(h.ctx, h.router, h.conn, buffer, h.metadata, destination)
|
||||
}
|
||||
|
||||
func (h *dnsHijacker) Close() error {
|
||||
if h.onClose != nil {
|
||||
h.onClose(nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ func (r *Router) routeConnection(ctx context.Context, conn net.Conn, metadata ad
|
||||
for _, buffer := range buffers {
|
||||
conn = bufio.NewCachedConn(conn, buffer)
|
||||
}
|
||||
r.hijackDNSStream(ctx, conn, metadata)
|
||||
N.CloseOnHandshakeFailure(conn, onClose, r.hijackDNSStream(ctx, conn, metadata))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -233,7 +233,7 @@ func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, m
|
||||
N.CloseOnHandshakeFailure(conn, onClose, action.Error(ctx))
|
||||
return nil
|
||||
case *rule.RuleActionHijackDNS:
|
||||
r.hijackDNSPacket(ctx, conn, packetBuffers, metadata)
|
||||
r.hijackDNSPacket(ctx, conn, packetBuffers, metadata, onClose)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -425,6 +425,9 @@ match:
|
||||
Fqdn: metadata.Destination.Fqdn,
|
||||
}
|
||||
}
|
||||
if routeOptions.OverrideTunnelDestination != "" {
|
||||
metadata.TunnelDestination = routeOptions.OverrideTunnelDestination
|
||||
}
|
||||
if routeOptions.NetworkStrategy != nil {
|
||||
metadata.NetworkStrategy = routeOptions.NetworkStrategy
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ func NewRuleAction(ctx context.Context, logger logger.ContextLogger, action opti
|
||||
RuleActionRouteOptions: RuleActionRouteOptions{
|
||||
OverrideAddress: M.ParseSocksaddrHostPort(action.RouteOptions.OverrideAddress, 0),
|
||||
OverridePort: action.RouteOptions.OverridePort,
|
||||
OverrideTunnelDestination: action.RouteOptions.OverrideTunnelDestination,
|
||||
NetworkStrategy: (*C.NetworkStrategy)(action.RouteOptions.NetworkStrategy),
|
||||
FallbackDelay: time.Duration(action.RouteOptions.FallbackDelay),
|
||||
UDPDisableDomainUnmapping: action.RouteOptions.UDPDisableDomainUnmapping,
|
||||
@@ -147,6 +148,7 @@ func (r *RuleActionRoute) String() string {
|
||||
type RuleActionRouteOptions struct {
|
||||
OverrideAddress M.Socksaddr
|
||||
OverridePort uint16
|
||||
OverrideTunnelDestination string
|
||||
NetworkStrategy *C.NetworkStrategy
|
||||
NetworkType []C.InterfaceType
|
||||
FallbackNetworkType []C.InterfaceType
|
||||
@@ -168,6 +170,9 @@ func (r *RuleActionRouteOptions) String() string {
|
||||
if r.OverridePort > 0 {
|
||||
descriptions = append(descriptions, F.ToString("override-port=", r.OverridePort))
|
||||
}
|
||||
if r.OverrideTunnelDestination != "" {
|
||||
descriptions = append(descriptions, F.ToString("override-tunnel-destination=", r.OverrideTunnelDestination))
|
||||
}
|
||||
if r.NetworkStrategy != nil {
|
||||
descriptions = append(descriptions, F.ToString("network-strategy=", r.NetworkStrategy))
|
||||
}
|
||||
|
||||
@@ -189,6 +189,16 @@ func NewDefaultRule(ctx context.Context, logger log.ContextLogger, options optio
|
||||
rule.destinationPortItems = append(rule.destinationPortItems, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
if len(options.TunnelSource) > 0 {
|
||||
item := NewTunnelSourceItem(options.TunnelSource)
|
||||
rule.items = append(rule.items, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
if len(options.TunnelDestination) > 0 {
|
||||
item := NewTunnelDestinationItem(options.TunnelDestination)
|
||||
rule.items = append(rule.items, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
if len(options.ProcessName) > 0 {
|
||||
item := NewProcessItem(options.ProcessName)
|
||||
rule.items = append(rule.items, item)
|
||||
|
||||
@@ -180,6 +180,16 @@ func NewDefaultDNSRule(ctx context.Context, logger log.ContextLogger, options op
|
||||
rule.destinationPortItems = append(rule.destinationPortItems, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
if len(options.TunnelSource) > 0 {
|
||||
item := NewTunnelSourceItem(options.TunnelSource)
|
||||
rule.items = append(rule.items, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
if len(options.TunnelDestination) > 0 {
|
||||
item := NewTunnelDestinationItem(options.TunnelDestination)
|
||||
rule.items = append(rule.items, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
if len(options.ProcessName) > 0 {
|
||||
item := NewProcessItem(options.ProcessName)
|
||||
rule.items = append(rule.items, item)
|
||||
|
||||
@@ -121,6 +121,16 @@ func NewDefaultHeadlessRule(ctx context.Context, options option.DefaultHeadlessR
|
||||
rule.destinationPortItems = append(rule.destinationPortItems, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
if len(options.TunnelSource) > 0 {
|
||||
item := NewTunnelSourceItem(options.TunnelSource)
|
||||
rule.items = append(rule.items, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
if len(options.TunnelDestination) > 0 {
|
||||
item := NewTunnelDestinationItem(options.TunnelDestination)
|
||||
rule.items = append(rule.items, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
if len(options.ProcessName) > 0 {
|
||||
item := NewProcessItem(options.ProcessName)
|
||||
rule.items = append(rule.items, item)
|
||||
|
||||
35
route/rule/rule_item_tunnel_destination.go
Normal file
35
route/rule/rule_item_tunnel_destination.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package rule
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
)
|
||||
|
||||
var _ RuleItem = (*TunnelDestinationItem)(nil)
|
||||
|
||||
type TunnelDestinationItem struct {
|
||||
destinations []string
|
||||
destinationMap map[string]bool
|
||||
}
|
||||
|
||||
func NewTunnelDestinationItem(destinations []string) *TunnelDestinationItem {
|
||||
rule := &TunnelDestinationItem{destinations, make(map[string]bool)}
|
||||
for _, destination := range destinations {
|
||||
rule.destinationMap[destination] = true
|
||||
}
|
||||
return rule
|
||||
}
|
||||
|
||||
func (r *TunnelDestinationItem) Match(metadata *adapter.InboundContext) bool {
|
||||
return r.destinationMap[metadata.TunnelDestination]
|
||||
}
|
||||
|
||||
func (r *TunnelDestinationItem) String() string {
|
||||
if len(r.destinations) == 1 {
|
||||
return F.ToString("tunnel_destination=", r.destinations[0])
|
||||
} else {
|
||||
return F.ToString("tunnel_destination=[", strings.Join(r.destinations, " "), "]")
|
||||
}
|
||||
}
|
||||
35
route/rule/rule_item_tunnel_source.go
Normal file
35
route/rule/rule_item_tunnel_source.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package rule
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
)
|
||||
|
||||
var _ RuleItem = (*TunnelSourceItem)(nil)
|
||||
|
||||
type TunnelSourceItem struct {
|
||||
sources []string
|
||||
sourceMap map[string]bool
|
||||
}
|
||||
|
||||
func NewTunnelSourceItem(sources []string) *TunnelSourceItem {
|
||||
rule := &TunnelSourceItem{sources, make(map[string]bool)}
|
||||
for _, source := range sources {
|
||||
rule.sourceMap[source] = true
|
||||
}
|
||||
return rule
|
||||
}
|
||||
|
||||
func (r *TunnelSourceItem) Match(metadata *adapter.InboundContext) bool {
|
||||
return r.sourceMap[metadata.TunnelSource]
|
||||
}
|
||||
|
||||
func (r *TunnelSourceItem) String() string {
|
||||
if len(r.sources) == 1 {
|
||||
return F.ToString("tunnel_source=", r.sources[0])
|
||||
} else {
|
||||
return F.ToString("tunnel_source=[", strings.Join(r.sources, " "), "]")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user