Compare commits

..

17 Commits

Author SHA1 Message Date
Sergei Maklagin
209b89a4a3 Add tunnel 2025-07-06 18:31:06 +03:00
Sergei Maklagin
765111a552 Merge tag 'v1.11.14' into HEAD 2025-06-19 20:18:51 +03:00
世界
9b8ab3e61e Bump version 2025-06-19 11:57:44 +08:00
dyhkwong
47f18e823a Fix: macOS udp find process should use unspecified fallback
be8d63ba8f
2025-06-18 08:34:59 +08:00
世界
2d1b824b62 Fix gLazyConn race 2025-06-17 14:24:11 +08:00
Sergei Maklagin
824eac453b Add new examples 2025-06-15 22:23:42 +03:00
Sergei Maklagin
85a1a8a53b Format examples 2025-06-15 22:23:25 +03:00
Sergei Maklagin
ae9e7aa5f4 Fix Range 2025-06-15 22:21:58 +03:00
Sergei Maklagin
52b71c6f00 Add sdns transport 2025-06-15 20:47:05 +03:00
Sergei Maklagin
5d04673783 Fix XHTTP Fqdn 2025-06-15 19:41:22 +03:00
Sergei Maklagin
307c429715 Fix WARP endpoint error 2025-06-15 19:40:42 +03:00
Sergei Maklagin
6801b58d96 Fix interrupt_exist_connections 2025-06-15 19:32:14 +03:00
Sergei Maklagin
6272596ebc Add unified delay 2025-06-15 19:24:09 +03:00
Sergei Maklagin
7c4c2d5ca8 Add mieru protocol 2025-06-15 18:04:27 +03:00
世界
d511698f3f Fix slowOpenConn 2025-06-12 08:05:04 +08:00
世界
cb435ea232 Fix default network strategy 2025-06-12 08:05:04 +08:00
世界
43a9016c83 Fix leak in hijack-dns 2025-06-06 14:28:09 +08:00
62 changed files with 2264 additions and 306 deletions

View File

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

4
box.go
View File

@@ -15,6 +15,7 @@ import (
"github.com/sagernet/sing-box/common/dialer"
"github.com/sagernet/sing-box/common/taskmonitor"
"github.com/sagernet/sing-box/common/tls"
"github.com/sagernet/sing-box/common/urltest"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/experimental"
"github.com/sagernet/sing-box/experimental/cachefile"
@@ -114,6 +115,9 @@ func New(options Options) (*Box, error) {
if experimentalOptions.V2RayAPI != nil && experimentalOptions.V2RayAPI.Listen != "" {
needV2RayAPI = true
}
if experimentalOptions.UnifiedDelay != nil && experimentalOptions.UnifiedDelay.Enabled {
ctx = urltest.ContextWithIsUnifiedDelay(ctx)
}
platformInterface := service.FromContext[platform.Interface](ctx)
var defaultLogWriter io.Writer
if platformInterface != nil {

View File

@@ -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)

View File

@@ -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)

View File

@@ -3,6 +3,7 @@ package interrupt
import (
"net"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/x/list"
)
@@ -73,3 +74,32 @@ func (c *PacketConn) WriterReplaceable() bool {
func (c *PacketConn) Upstream() any {
return c.PacketConn
}
type SingPacketConn struct {
N.PacketConn
group *Group
element *list.Element[*groupConnItem]
}
/*func (c *SingPacketConn) MarkAsInternal() {
c.element.Value.internal = true
}*/
func (c *SingPacketConn) Close() error {
c.group.access.Lock()
defer c.group.access.Unlock()
c.group.connections.Remove(c.element)
return c.PacketConn.Close()
}
func (c *SingPacketConn) ReaderReplaceable() bool {
return true
}
func (c *SingPacketConn) WriterReplaceable() bool {
return true
}
func (c *SingPacketConn) Upstream() any {
return c.PacketConn
}

View File

@@ -5,6 +5,7 @@ import (
"net"
"sync"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/x/list"
)
@@ -36,6 +37,13 @@ func (g *Group) NewPacketConn(conn net.PacketConn, isExternal bool) net.PacketCo
return &PacketConn{PacketConn: conn, group: g, element: item}
}
func (g *Group) NewSingPacketConn(conn N.PacketConn, isExternal bool) N.PacketConn {
g.access.Lock()
defer g.access.Unlock()
item := g.connections.PushBack(&groupConnItem{conn, isExternal})
return &SingPacketConn{PacketConn: conn, group: g, element: item}
}
func (g *Group) Interrupt(interruptExternalConnections bool) {
g.access.Lock()
defer g.access.Unlock()

View File

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

13
common/urltest/context.go Normal file
View File

@@ -0,0 +1,13 @@
package urltest
import "context"
type contextKeyIsUnifiedDelay struct{}
func ContextWithIsUnifiedDelay(ctx context.Context) context.Context {
return context.WithValue(ctx, contextKeyIsUnifiedDelay{}, true)
}
func IsUnifiedDelayFromContext(ctx context.Context) bool {
return ctx.Value(contextKeyIsUnifiedDelay{}) != nil
}

View File

@@ -122,6 +122,15 @@ func URLTest(ctx context.Context, link string, detour N.Dialer) (t uint16, err e
return
}
resp.Body.Close()
if IsUnifiedDelayFromContext(ctx) {
second := time.Now()
resp, err = client.Do(req)
if err != nil {
return
}
resp.Body.Close()
start = second
}
t = uint16(time.Since(start) / time.Millisecond)
return
}

View File

@@ -24,27 +24,36 @@ func (c *Range) MarshalJSON() ([]byte, error) {
}
func (c *Range) UnmarshalJSON(content []byte) error {
var rangeValue struct {
From int32 `json:"from"`
To int32 `json:"to"`
}
var stringValue string
err := json.Unmarshal(content, &stringValue)
if err != nil {
return err
if err == nil {
parts := strings.Split(stringValue, "-")
if len(parts) != 2 {
return E.New("invalid length of range parts")
}
from, err := strconv.ParseInt(parts[0], 10, 32)
if err != nil {
return err
}
to, err := strconv.ParseInt(parts[1], 10, 32)
if err != nil {
return err
}
rangeValue.From, rangeValue.To = int32(from), int32(to)
} else {
err := json.Unmarshal(content, &rangeValue)
if err != nil {
return err
}
}
parts := strings.Split(stringValue, "-")
if len(parts) != 2 {
return E.New("invalid length of range parts")
}
from, err := strconv.ParseInt(parts[0], 10, 32)
if err != nil {
return err
}
to, err := strconv.ParseInt(parts[1], 10, 32)
if err != nil {
return err
}
if from > to {
if rangeValue.From > rangeValue.To {
return E.New("invalid range")
}
*c = Range{int32(from), int32(to)}
*c = Range{rangeValue.From, rangeValue.To}
return nil
}

View File

@@ -20,10 +20,13 @@ const (
TypeTor = "tor"
TypeSSH = "ssh"
TypeShadowTLS = "shadowtls"
TypeMieru = "mieru"
TypeShadowsocksR = "shadowsocksr"
TypeVLESS = "vless"
TypeTUIC = "tuic"
TypeHysteria2 = "hysteria2"
TypeTunnelClient = "tunnel_client"
TypeTunnelServer = "tunnel_server"
)
const (
@@ -79,10 +82,16 @@ func ProxyDisplayName(proxyType string) string {
return "TUIC"
case TypeHysteria2:
return "Hysteria2"
case TypeMieru:
return "Mieru"
case TypeSelector:
return "Selector"
case TypeURLTest:
return "URLTest"
case TypeTunnelClient:
return "Tunnel Client"
case TypeTunnelServer:
return "Tunnel Server"
default:
return "Unknown"
}

View File

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

View File

@@ -30,6 +30,7 @@
| `shadowtls` | [ShadowTLS](./shadowtls/) |
| `tuic` | [TUIC](./tuic/) |
| `hysteria2` | [Hysteria2](./hysteria2/) |
| `mieru` | [Mieru](./mieru/) |
| `tor` | [Tor](./tor/) |
| `ssh` | [SSH](./ssh/) |
| `dns` | [DNS](./dns/) |

View File

@@ -30,6 +30,7 @@
| `shadowtls` | [ShadowTLS](./shadowtls/) |
| `tuic` | [TUIC](./tuic/) |
| `hysteria2` | [Hysteria2](./hysteria2/) |
| `mieru` | [Mieru](./mieru/) |
| `tor` | [Tor](./tor/) |
| `ssh` | [SSH](./ssh/) |
| `dns` | [DNS](./dns/) |

View File

@@ -0,0 +1,71 @@
---
icon: material/new-box
---
### Structure
```json
{
"type": "mieru",
"tag": "mieru-out",
"server": "127.0.0.1",
"server_port": 1080,
"server_ports": [
"9000-9010",
"9020-9030"
],
"transport": "TCP",
"username": "asdf",
"password": "hjkl",
"multiplexing": "MULTIPLEXING_LOW",
... // Dial Fields
}
```
### Fields
#### server
==Required==
The server address.
#### server_port
The server port.
Must set at least one field between `server_port` and `server_ports`.
#### server_ports
Server port range list.
Must set at least one field between `server_port` and `server_ports`.
#### transport
==Required==
Transmission protocol. The only allowed value is `TCP`.
#### username
==Required==
mieru user name.
#### password
==Required==
mieru password.
#### multiplexing
Multiplexing level. Supported values are `MULTIPLEXING_OFF`, `MULTIPLEXING_LOW`, `MULTIPLEXING_MIDDLE`, `MULTIPLEXING_HIGH`. `MULTIPLEXING_OFF` disables multiplexing.
### Dial Fields
See [Dial Fields](/configuration/shared/dial/) for details.

View File

@@ -0,0 +1,71 @@
---
icon: material/new-box
---
### 结构
```json
{
"type": "mieru",
"tag": "mieru-out",
"server": "127.0.0.1",
"server_port": 1080,
"server_ports": [
"9000-9010",
"9020-9030"
],
"transport": "TCP",
"username": "asdf",
"password": "hjkl",
"multiplexing": "MULTIPLEXING_LOW",
... // 拨号字段
}
```
### 字段
#### server
==必填==
服务器地址。
#### server_port
服务器端口。
必须填写 `server_port``server_ports` 中至少一项。
#### server_ports
服务器端口范围列表。
必须填写 `server_port``server_ports` 中至少一项。
#### transport
==必填==
通信协议。仅可设为 `TCP`
#### username
==必填==
mieru 用户名。
#### password
==必填==
mieru 密码。
#### multiplexing
多路复用设置。可以设为 `MULTIPLEXING_OFF``MULTIPLEXING_LOW``MULTIPLEXING_MIDDLE``MULTIPLEXING_HIGH`。其中 `MULTIPLEXING_OFF` 会关闭多路复用功能。
### 拨号字段
参阅 [拨号字段](/zh/configuration/shared/dial/)。

View File

@@ -1,84 +1,71 @@
{
"log":{
"level":"error"
"log": {
"level": "error"
},
"dns":{
"servers":[
"dns": {
"servers": [
{
"address":"local",
"detour":"direct"
"address": "local",
"detour": "direct"
}
]
},
"endpoints":[
"endpoints": [
{
"type":"wireguard",
"tag":"wireguard-out",
"system":false,
"name":"",
"mtu":1408,
"address":[],
"private_key":"",
"listen_port":10000,
"peers":[
"type": "wireguard",
"tag": "wireguard-out",
"mtu": 1408,
"address": null,
"private_key": "",
"listen_port": 10000,
"peers": [
{
"address":"example.com",
"port":10001,
"public_key":"",
"pre_shared_key":"",
"allowed_ips":[],
"persistent_keepalive_interval":0,
"reserved":[
0,
0,
0
]
"address": "example.com",
"port": 10001,
"reserved": "AAAA"
}
],
"udp_timeout":"",
"workers":0,
"amnezia":{
"jc":120,
"jmin":23,
"jmax":911,
"s1":0,
"s2":0,
"h1":1,
"h2":2,
"h3":3,
"h4":4
"udp_timeout": "5m0s",
"amnezia": {
"jc": 120,
"jmin": 23,
"jmax": 911,
"h1": 1,
"h2": 2,
"h3": 3,
"h4": 4
}
}
],
"inbounds":[
"inbounds": [
{
"type":"mixed",
"tag":"mixed-in",
"listen_port":7897
"type": "mixed",
"tag": "mixed-in",
"listen_port": 7897
}
],
"outbounds":[
"outbounds": [
{
"type":"direct",
"tag":"direct"
"type": "direct",
"tag": "direct"
},
{
"type":"dns",
"tag":"dns-out"
"type": "dns",
"tag": "dns-out"
}
],
"route":{
"rules":[
"route": {
"rules": [
{
"protocol":"dns",
"outbound":"dns-out"
"protocol": "dns",
"outbound": "dns-out"
},
{
"port":53,
"outbound":"dns-out"
"port": 53,
"outbound": "dns-out"
}
],
"final":"wireguard-out",
"auto_detect_interface":true
"final": "wireguard-out",
"auto_detect_interface": true
}
}

View File

@@ -0,0 +1,56 @@
{
"log": {
"level": "error"
},
"dns": {
"servers": [
{
"address": "local",
"detour": "direct"
}
]
},
"inbounds": [
{
"type": "mixed",
"tag": "mixed-in",
"listen_port": 7897
}
],
"outbounds": [
{
"type": "direct",
"tag": "direct"
},
{
"type": "mieru",
"tag": "mieru-out",
"server": "example.com",
"server_port": 27017,
"server_ports": "27017-27019",
"transport": "TCP",
"username": "username",
"password": "password",
"multiplexing": "MULTIPLEXING_LOW"
// Dial Fields
},
{
"type": "dns",
"tag": "dns-out"
}
],
"route": {
"rules": [
{
"protocol": "dns",
"outbound": "dns-out"
},
{
"port": 53,
"outbound": "dns-out"
}
],
"final": "mieru-out",
"auto_detect_interface": true
}
}

44
examples/sdns/client.json Normal file
View File

@@ -0,0 +1,44 @@
{
"log": {
"level": "error"
},
"dns": {
"servers": [
{
"address": "sdns://AQMAAAAAAAAAETk0LjE0MC4xNS4xNTo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20",
"detour": "direct"
}
]
},
"inbounds": [
{
"type": "mixed",
"tag": "mixed-in",
"listen_port": 7897
}
],
"outbounds": [
{
"type": "direct",
"tag": "direct"
},
{
"type": "dns",
"tag": "dns-out"
}
],
"route": {
"rules": [
{
"protocol": "dns",
"outbound": "dns-out"
},
{
"port": 53,
"outbound": "dns-out"
}
],
"final": "direct",
"auto_detect_interface": true
}
}

View 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
}
}

View 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
}
}

View 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
}
}

View 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
}
}

View 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
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View 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
}
}

View 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
}
}

View 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
}
}

View File

@@ -0,0 +1,79 @@
{
"log": {
"level": "error"
},
"dns": {
"servers": [
{
"address": "local",
"detour": "direct"
}
]
},
"endpoints": [
{
"type": "warp",
"tag": "warp-out",
"listen_port": 10000,
"udp_timeout": "5m0s",
"amnezia": {
"jc": 120,
"jmin": 23,
"jmax": 911,
"h1": 1,
"h2": 2,
"h3": 3,
"h4": 4
},
"profile": {
"detour": "direct"
}
}
],
"inbounds": [
{
"type": "mixed",
"tag": "mixed-in",
"listen_port": 7897
}
],
"outbounds": [
{
"type": "direct",
"tag": "direct"
},
{
"type": "selector",
"tag": "select-out",
"outbounds": [
"direct",
"warp-out"
],
"default": "direct",
"interrupt_exist_connections": true
},
{
"type": "dns",
"tag": "dns-out"
}
],
"route": {
"rules": [
{
"protocol": "dns",
"outbound": "dns-out"
},
{
"port": 53,
"outbound": "dns-out"
}
],
"final": "direct",
"auto_detect_interface": true
},
"experimental": {
"unified_delay": {
"enabled": true
}
}
}

View File

@@ -1,79 +1,75 @@
{
"log":{
"level":"error"
"log": {
"level": "error"
},
"dns":{
"servers":[
"dns": {
"servers": [
{
"address":"local",
"detour":"direct"
"address": "local",
"detour": "direct"
}
]
},
"endpoints":[
"endpoints": [
{
"type":"warp",
"tag":"warp-out",
"system":false,
"name":"",
"listen_port":10000,
"udp_timeout":"",
"workers":0,
"profile":{
"detour":"direct",
"recreate":false,
// for getting existing WARP device profile
"id":"",
"private_key":"",
"auth_token":""
"type": "warp",
"tag": "warp-out",
"listen_port": 10000,
"udp_timeout": "5m0s",
"amnezia": {
"jc": 120,
"jmin": 23,
"jmax": 911,
"h1": 1,
"h2": 2,
"h3": 3,
"h4": 4
},
"amnezia":{
"jc":120,
"jmin":23,
"jmax":911,
"h1":1,
"h2":2,
"h3":3,
"h4":4
"profile": {
"detour": "direct",
// for getting existing WARP device profile
"id": "",
"private_key": "",
"auth_token": ""
}
// Dial Fields
}
],
"inbounds":[
"inbounds": [
{
"type":"mixed",
"tag":"mixed-in",
"listen_port":7897
"type": "mixed",
"tag": "mixed-in",
"listen_port": 7897
}
],
"outbounds":[
"outbounds": [
{
"type":"direct",
"tag":"direct"
"type": "direct",
"tag": "direct"
},
{
"type":"dns",
"tag":"dns-out"
"type": "dns",
"tag": "dns-out"
}
],
"route":{
"rules":[
"route": {
"rules": [
{
"protocol":"dns",
"outbound":"dns-out"
"protocol": "dns",
"outbound": "dns-out"
},
{
"port":53,
"outbound":"dns-out"
"port": 53,
"outbound": "dns-out"
}
],
"final":"warp-out",
"auto_detect_interface":true
"final": "warp-out",
"auto_detect_interface": true
},
"experimental":{
"cache_file":{
"enabled":true,
"store_warp_config":true
"experimental": {
"cache_file": {
"enabled": true,
"store_warp_config": true
}
}
}

View File

@@ -1,97 +1,98 @@
{
"log":{
"level":"error"
"log": {
"level": "error"
},
"dns":{
"servers":[
"dns": {
"servers": [
{
"address":"local",
"detour":"direct"
"address": "local",
"detour": "direct"
}
]
},
"inbounds":[
"inbounds": [
{
"type":"mixed",
"tag":"mixed-in",
"listen_port":7897
"type": "mixed",
"tag": "mixed-in",
"listen_port": 7897
}
],
"outbounds":[
"outbounds": [
{
"type":"direct",
"tag":"direct"
"type": "direct",
"tag": "direct"
},
{
"type":"vless",
"tag":"vless-out",
"server":"example.com",
"server_port":443,
"uuid":"3179dce2-2ff9-413c-85b4-c1d53ed41668",
"tls":{
"enabled":true,
"server_name":"example.com",
"alpn":[
"h2" // h3 for QUIC
]
"type": "vless",
"tag": "vless-out",
"server": "example.com",
"server_port": 443,
"uuid": "3179dce2-2ff9-413c-85b4-c1d53ed41668",
"tls": {
"enabled": true,
"server_name": "example.com",
"alpn": "h2" // h3 for QUIC
},
"packet_encoding":"",
"transport":{
"type":"xhttp",
"mode":"stream-up",
"host":"example.com",
"path":"/xhttp",
"domain_strategy":"prefer_ipv4",
"xmux":{
"max_concurrency":"0-1",
"max_connections":"0-1",
"c_max_reuse_times":"0-1",
"h_max_request_times":"0-1",
"h_max_reusable_secs":"0-1",
"h_keep_alive_period":60
"transport": {
"type": "xhttp",
"mode": "stream-up",
"host": "example.com",
"path": "/xhttp",
"domain_strategy": "prefer_ipv4",
"xmux": {
"max_concurrency": "0-1",
"max_connections": "0-1",
"c_max_reuse_times": "0-1",
"h_max_request_times": "0-1",
"h_max_reusable_secs": "0-1",
"h_keep_alive_period": 60
},
"download":{
"server":"example.com",
"server_port":443,
"host":"example.com",
"path":"/xhttp",
"domain_strategy":"prefer_ipv4",
"detour":"direct",
"tls":{
"enabled":true,
"server_name":"example.com",
"alpn":[
"h2" // h3 for QUIC
]
"download": {
"mode": "",
"host": "example.com",
"path": "/xhttp",
"domain_strategy": "prefer_ipv4",
"x_padding_bytes": "0-0",
"sc_max_each_post_bytes": "0-0",
"sc_min_posts_interval_ms": "0-0",
"sc_stream_up_server_secs": "0-0",
"xmux": {
"max_concurrency": "0-1",
"max_connections": "0-1",
"c_max_reuse_times": "0-1",
"h_max_request_times": "0-1",
"h_max_reusable_secs": "0-1",
"h_keep_alive_period": 60
},
"xmux":{
"max_concurrency":"0-1",
"max_connections":"0-1",
"c_max_reuse_times":"0-1",
"h_max_request_times":"0-1",
"h_max_reusable_secs":"0-1",
"h_keep_alive_period":60
}
"server": "example.com",
"server_port": 443,
"tls": {
"enabled": true,
"server_name": "example.com",
"alpn": "h2" // h3 for QUIC
},
"detour": "direct"
}
}
},
"packet_encoding": ""
},
{
"type":"dns",
"tag":"dns-out"
"type": "dns",
"tag": "dns-out"
}
],
"route":{
"rules":[
"route": {
"rules": [
{
"protocol":"dns",
"outbound":"dns-out"
"protocol": "dns",
"outbound": "dns-out"
},
{
"port":53,
"outbound":"dns-out"
"port": 53,
"outbound": "dns-out"
}
],
"final":"vless-out",
"auto_detect_interface":true
"final": "vless-out",
"auto_detect_interface": true
}
}

View File

@@ -1,65 +1,63 @@
{
"log":{
"level":"error"
"log": {
"level": "error"
},
"dns":{
"servers":[
"dns": {
"servers": [
{
"address":"local",
"detour":"direct"
"address": "local",
"detour": "direct"
}
]
},
"inbounds":[
"inbounds": [
{
"type":"vless",
"tag":"vless-out",
"listen":"0.0.0.0",
"listen_port":443,
"tls":{
"enabled":true,
"server_name":"example.com",
"certificate_path":"/path/to/fullchain.pem",
"key_path":"/path/to/privkey.pem",
"alpn":[
"h2" // h3 for QUIC
]
},
"users":[
"type": "vless",
"tag": "vless-out",
"listen": "0.0.0.0",
"listen_port": 443,
"users": [
{
"name":"user",
"uuid":"3179dce2-2ff9-413c-85b4-c1d53ed41668"
"name": "user",
"uuid": "3179dce2-2ff9-413c-85b4-c1d53ed41668"
}
],
"transport":{
"type":"xhttp",
"mode":"stream-up",
"path":"/xhttp"
"tls": {
"enabled": true,
"server_name": "example.com",
"alpn": "h2", // h3 for QUIC
"certificate_path": "/path/to/fullchain.pem",
"key_path": "/path/to/privkey.pem"
},
"transport": {
"type": "xhttp",
"mode": "stream-up",
"path": "/xhttp",
}
}
],
"outbounds":[
"outbounds": [
{
"type":"direct",
"tag":"direct"
"type": "direct",
"tag": "direct"
},
{
"type":"dns",
"tag":"dns-out"
"type": "dns",
"tag": "dns-out"
}
],
"route":{
"rules":[
"route": {
"rules": [
{
"protocol":"dns",
"outbound":"dns-out"
"protocol": "dns",
"outbound": "dns-out"
},
{
"port":53,
"outbound":"dns-out"
"port": 53,
"outbound": "dns-out"
}
],
"final":"direct",
"auto_detect_interface":true
"final": "direct",
"auto_detect_interface": true
}
}
}

View File

@@ -69,6 +69,10 @@ func NewService(configContent string, platformInterface PlatformInterface) (*Box
cancel()
return nil, E.Cause(err, "create service")
}
experimentalOptions := common.PtrValueOrDefault(options.Experimental)
if experimentalOptions.UnifiedDelay != nil && experimentalOptions.UnifiedDelay.Enabled {
ctx = urltest.ContextWithIsUnifiedDelay(ctx)
}
runtimeDebug.FreeOSMemory()
return &BoxService{
ctx: ctx,

18
go.mod
View File

@@ -1,6 +1,6 @@
module github.com/sagernet/sing-box
go 1.24
go 1.24.1
toolchain go1.24.3
@@ -8,6 +8,7 @@ require (
github.com/caddyserver/certmagic v0.20.0
github.com/cloudflare/circl v1.6.1
github.com/cretz/bine v0.2.0
github.com/enfein/mieru/v3 v3.13.0
github.com/go-chi/chi/v5 v5.2.1
github.com/go-chi/render v1.0.3
github.com/gofrs/uuid/v5 v5.3.2
@@ -36,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
@@ -47,11 +48,11 @@ require (
go.uber.org/zap v1.27.0
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
golang.org/x/crypto v0.38.0
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394
golang.org/x/mod v0.24.0
golang.org/x/net v0.40.0
golang.org/x/sys v0.33.0
golang.org/x/time v0.7.0
golang.org/x/time v0.11.0
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
google.golang.org/grpc v1.72.1
google.golang.org/protobuf v1.36.6
@@ -59,6 +60,9 @@ require (
)
require (
github.com/AdguardTeam/golibs v0.32.7 // indirect
github.com/ameshkov/dnscrypt/v2 v2.4.0 // indirect
github.com/ameshkov/dnsstamps v1.0.3 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a // indirect
github.com/onsi/ginkgo/v2 v2.9.7 // indirect
@@ -108,10 +112,14 @@ require (
github.com/zeebo/blake3 v0.2.3 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250311190419-81fb87f6b8bf // indirect
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/blake3 v1.4.1 // indirect
)
replace github.com/sagernet/wireguard-go => github.com/getlantern/wireguard-go v0.0.1-beta.5.0.20250303165430-793006c422ec
replace github.com/sagernet/sing-dns => github.com/shtorm-7/sing-dns v0.4.5-extended-1.0.0
replace github.com/ameshkov/dnscrypt/v2 => github.com/shtorm-7/dnscrypt/v2 v2.4.0-extended-1.0.0

44
go.sum
View File

@@ -1,5 +1,9 @@
github.com/AdguardTeam/golibs v0.32.7 h1:3dmGlAVgmvquCCwHsvEl58KKcRAK3z1UnjMnwSIeDH4=
github.com/AdguardTeam/golibs v0.32.7/go.mod h1:bE8KV1zqTzgZjmjFyBJ9f9O5DEKO717r7e57j1HclJA=
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
github.com/ameshkov/dnsstamps v1.0.3 h1:Srzik+J9mivH1alRACTbys2xOxs0lRH9qnTA7Y1OYVo=
github.com/ameshkov/dnsstamps v1.0.3/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/caddyserver/certmagic v0.20.0 h1:bTw7LcEZAh9ucYCRXyCpIrSAGplplI0vGYJ4BpCQ/Fc=
@@ -16,6 +20,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1 h1:CaO/zOnF8VvUfEbhRatPcwKVWamvbYd8tQGRWacE9kU=
github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1/go.mod h1:+hnT3ywWDTAFrW5aE+u2Sa/wT555ZqwoCS+pk3p6ry4=
github.com/enfein/mieru/v3 v3.13.0 h1:eGyxLGkb+lut9ebmx+BGwLJ5UMbEc/wGIYO0AXEKy98=
github.com/enfein/mieru/v3 v3.13.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/getlantern/wireguard-go v0.0.1-beta.5.0.20250303165430-793006c422ec h1:jXekDkSozctYj5JQlV1mCsIW+qHpIkgSFhOIpgRSB1I=
@@ -133,8 +139,6 @@ github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4Wk
github.com/sagernet/sing v0.6.9/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing v0.6.10 h1:Jey1tePgH9bjFuK1fQI3D9T+bPOQ4SdHMjuS4sYjDv4=
github.com/sagernet/sing v0.6.10/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing-dns v0.4.5 h1:D9REN14qx2FTrZRBrtFLL99f2CuFzQ9S7mIf8uV5hZI=
github.com/sagernet/sing-dns v0.4.5/go.mod h1:dweQs54ng2YGzoJfz+F9dGuDNdP5pJ3PLeggnK5VWc8=
github.com/sagernet/sing-mux v0.3.2 h1:meZVFiiStvHThb/trcpAkCrmtJOuItG5Dzl1RRP5/NE=
github.com/sagernet/sing-mux v0.3.2/go.mod h1:pht8iFY4c9Xltj7rhVd208npkNaeCxzyXCgulDPLUDA=
github.com/sagernet/sing-quic v0.4.3 h1:OZ/kGvSzjtYg+t0DY3F606hlT5LeiQQXDxfBopcRryQ=
@@ -145,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=
@@ -155,6 +159,10 @@ github.com/sagernet/utls v1.6.7 h1:Ep3+aJ8FUGGta+II2IEVNUc3EDhaRCZINWkj/LloIA8=
github.com/sagernet/utls v1.6.7/go.mod h1:Uua1TKO/FFuAhLr9rkaVnnrTmmiItzDjv1BUb2+ERwM=
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc=
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854/go.mod h1:LtfoSK3+NG57tvnVEHgcuBW9ujgE8enPSgzgwStwCAA=
github.com/shtorm-7/dnscrypt/v2 v2.4.0-extended-1.0.0 h1:e5s7RKBd2rIPR0StbvZ2vTVtJ5jDTsTk5wtIIapZTRg=
github.com/shtorm-7/dnscrypt/v2 v2.4.0-extended-1.0.0/go.mod h1:WpEFV2uhebXb8Jhes/5/fSdpmhGV8TL22RDaeWwV6hI=
github.com/shtorm-7/sing-dns v0.4.5-extended-1.0.0 h1:iR0BTDEYwuYfQHwiySBhDOUHaipcizGb54OmC1C6Pz0=
github.com/shtorm-7/sing-dns v0.4.5-extended-1.0.0/go.mod h1:HWuvH9L2ZwuffhzX5J5ZOjrhLQ8DB5qT9ie/YTXW1nU=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
@@ -191,16 +199,16 @@ github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
@@ -214,8 +222,8 @@ go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/W
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
@@ -240,8 +248,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
@@ -251,8 +259,8 @@ golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeu
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 h1:CawjfCvYQH2OU3/TnxLx97WDSUDRABfT18pCOYwc2GE=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6/go.mod h1:3rxYc4HtVcSG9gVaTs2GEBdehh+sYPOwKtyUWEOTb80=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250311190419-81fb87f6b8bf h1:dHDlF3CWxQkefK9IJx+O8ldY0gLygvrlYRBNbPqDWuY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250311190419-81fb87f6b8bf/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA=
google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=

View File

@@ -15,6 +15,7 @@ import (
"github.com/sagernet/sing-box/protocol/dns"
"github.com/sagernet/sing-box/protocol/group"
"github.com/sagernet/sing-box/protocol/http"
"github.com/sagernet/sing-box/protocol/mieru"
"github.com/sagernet/sing-box/protocol/mixed"
"github.com/sagernet/sing-box/protocol/naive"
"github.com/sagernet/sing-box/protocol/redirect"
@@ -25,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"
@@ -75,6 +77,7 @@ func OutboundRegistry() *outbound.Registry {
ssh.RegisterOutbound(registry)
shadowtls.RegisterOutbound(registry)
vless.RegisterOutbound(registry)
mieru.RegisterOutbound(registry)
registerQUICOutbounds(registry)
registerWireGuardOutbound(registry)
@@ -86,6 +89,9 @@ func OutboundRegistry() *outbound.Registry {
func EndpointRegistry() *endpoint.Registry {
registry := endpoint.NewRegistry()
tunnel.RegisterServerEndpoint(registry)
tunnel.RegisterClientEndpoint(registry)
registerWireGuardEndpoint(registry)
return registry

View File

@@ -3,10 +3,11 @@ package option
import "github.com/sagernet/sing/common/json/badoption"
type ExperimentalOptions struct {
CacheFile *CacheFileOptions `json:"cache_file,omitempty"`
ClashAPI *ClashAPIOptions `json:"clash_api,omitempty"`
V2RayAPI *V2RayAPIOptions `json:"v2ray_api,omitempty"`
Debug *DebugOptions `json:"debug,omitempty"`
CacheFile *CacheFileOptions `json:"cache_file,omitempty"`
ClashAPI *ClashAPIOptions `json:"clash_api,omitempty"`
V2RayAPI *V2RayAPIOptions `json:"v2ray_api,omitempty"`
UnifiedDelay *UnifiedDelayOptions `json:"unified_delay,omitempty"`
Debug *DebugOptions `json:"debug,omitempty"`
}
type CacheFileOptions struct {
@@ -53,3 +54,7 @@ type V2RayStatsServiceOptions struct {
Outbounds []string `json:"outbounds,omitempty"`
Users []string `json:"users,omitempty"`
}
type UnifiedDelayOptions struct {
Enabled bool `json:"enabled,omitempty"`
}

13
option/mieru.go Normal file
View File

@@ -0,0 +1,13 @@
package option
import "github.com/sagernet/sing/common/json/badoption"
type MieruOutboundOptions struct {
DialerOptions
ServerOptions
ServerPortRanges badoption.Listable[string] `json:"server_ports,omitempty"`
Transport string `json:"transport,omitempty"`
UserName string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
Multiplexing string `json:"multiplexing,omitempty"`
}

View File

@@ -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"`

View File

@@ -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"`

View File

@@ -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"`

View File

@@ -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
View 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"`
}

View File

@@ -157,6 +157,7 @@ func (s *Selector) ListenPacket(ctx context.Context, destination M.Socksaddr) (n
func (s *Selector) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
ctx = interrupt.ContextWithIsExternalConnection(ctx)
selected := s.selected.Load()
conn = s.interruptGroup.NewConn(conn, interrupt.IsExternalConnectionFromContext(ctx))
if outboundHandler, isHandler := selected.(adapter.ConnectionHandlerEx); isHandler {
outboundHandler.NewConnectionEx(ctx, conn, metadata, onClose)
} else {
@@ -167,6 +168,7 @@ func (s *Selector) NewConnectionEx(ctx context.Context, conn net.Conn, metadata
func (s *Selector) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
ctx = interrupt.ContextWithIsExternalConnection(ctx)
selected := s.selected.Load()
conn = s.interruptGroup.NewSingPacketConn(conn, interrupt.IsExternalConnectionFromContext(ctx))
if outboundHandler, isHandler := selected.(adapter.PacketConnectionHandlerEx); isHandler {
outboundHandler.NewPacketConnectionEx(ctx, conn, metadata, onClose)
} else {

View File

@@ -162,11 +162,13 @@ func (s *URLTest) ListenPacket(ctx context.Context, destination M.Socksaddr) (ne
func (s *URLTest) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
ctx = interrupt.ContextWithIsExternalConnection(ctx)
conn = s.group.interruptGroup.NewConn(conn, interrupt.IsExternalConnectionFromContext(ctx))
s.connection.NewConnection(ctx, s, conn, metadata, onClose)
}
func (s *URLTest) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
ctx = interrupt.ContextWithIsExternalConnection(ctx)
conn = s.group.interruptGroup.NewSingPacketConn(conn, interrupt.IsExternalConnectionFromContext(ctx))
s.connection.NewPacketConnection(ctx, s, conn, metadata, onClose)
}

245
protocol/mieru/outbound.go Normal file
View File

@@ -0,0 +1,245 @@
package mieru
import (
"context"
"fmt"
"net"
"os"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/adapter/outbound"
"github.com/sagernet/sing-box/common/dialer"
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"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
mieruclient "github.com/enfein/mieru/v3/apis/client"
mierucommon "github.com/enfein/mieru/v3/apis/common"
mierumodel "github.com/enfein/mieru/v3/apis/model"
mierupb "github.com/enfein/mieru/v3/pkg/appctl/appctlpb"
"google.golang.org/protobuf/proto"
)
type Outbound struct {
outbound.Adapter
dialer N.Dialer
logger log.ContextLogger
client mieruclient.Client
}
func RegisterOutbound(registry *outbound.Registry) {
outbound.Register(registry, C.TypeMieru, NewOutbound)
}
func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.MieruOutboundOptions) (adapter.Outbound, error) {
outboundDialer, err := dialer.New(ctx, options.DialerOptions)
if err != nil {
return nil, err
}
config, err := buildMieruClientConfig(options, mieruDialer{dialer: outboundDialer})
if err != nil {
return nil, fmt.Errorf("failed to build mieru client config: %w", err)
}
c := mieruclient.NewClient()
if err := c.Store(config); err != nil {
return nil, fmt.Errorf("failed to store mieru client config: %w", err)
}
if err := c.Start(); err != nil {
return nil, fmt.Errorf("failed to start mieru client: %w", err)
}
logger.InfoContext(ctx, "mieru client is started")
return &Outbound{
Adapter: outbound.NewAdapterWithDialerOptions(C.TypeMieru, tag, []string{N.NetworkTCP, N.NetworkUDP}, options.DialerOptions),
dialer: outboundDialer,
logger: logger,
client: c,
}, nil
}
func (o *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
ctx, metadata := adapter.ExtendContext(ctx)
metadata.Outbound = o.Tag()
metadata.Destination = destination
switch N.NetworkName(network) {
case N.NetworkTCP:
o.logger.InfoContext(ctx, "outbound connection to ", destination)
d, err := socksAddrToNetAddrSpec(destination, "tcp")
if err != nil {
return nil, E.Cause(err, "failed to convert destination address")
}
return o.client.DialContext(ctx, d)
case N.NetworkUDP:
o.logger.InfoContext(ctx, "outbound UoT packet connection to ", destination)
d, err := socksAddrToNetAddrSpec(destination, "udp")
if err != nil {
return nil, E.Cause(err, "failed to convert destination address")
}
streamConn, err := o.client.DialContext(ctx, d)
if err != nil {
return nil, err
}
return &streamer{
PacketConn: mierucommon.NewUDPAssociateWrapper(mierucommon.NewPacketOverStreamTunnel(streamConn)),
Remote: destination,
}, nil
default:
return nil, os.ErrInvalid
}
}
func (o *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
ctx, metadata := adapter.ExtendContext(ctx)
metadata.Outbound = o.Tag()
metadata.Destination = destination
o.logger.InfoContext(ctx, "outbound UoT packet connection to ", destination)
d, err := socksAddrToNetAddrSpec(destination, "udp")
if err != nil {
return nil, E.Cause(err, "failed to convert destination address")
}
streamConn, err := o.client.DialContext(ctx, d)
if err != nil {
return nil, err
}
return mierucommon.NewUDPAssociateWrapper(mierucommon.NewPacketOverStreamTunnel(streamConn)), nil
}
func (o *Outbound) Close() error {
return common.Close(o.client)
}
// mieruDialer is an adapter to mieru dialer interface.
type mieruDialer struct {
dialer N.Dialer
}
func (md mieruDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
addr := M.ParseSocksaddr(address)
return md.dialer.DialContext(ctx, network, addr)
}
var _ mierucommon.Dialer = (*mieruDialer)(nil)
// streamer converts a net.PacketConn to a net.Conn.
type streamer struct {
net.PacketConn
Remote net.Addr
}
var _ net.Conn = (*streamer)(nil)
func (s *streamer) Read(b []byte) (n int, err error) {
n, _, err = s.PacketConn.ReadFrom(b)
return
}
func (s *streamer) Write(b []byte) (n int, err error) {
return s.WriteTo(b, s.Remote)
}
func (s *streamer) RemoteAddr() net.Addr {
return s.Remote
}
// socksAddrToNetAddrSpec converts a Socksaddr object to NetAddrSpec, and overrides the network.
func socksAddrToNetAddrSpec(sa M.Socksaddr, network string) (mierumodel.NetAddrSpec, error) {
var nas mierumodel.NetAddrSpec
if err := nas.From(sa); err != nil {
return nas, err
}
nas.Net = network
return nas, nil
}
func buildMieruClientConfig(options option.MieruOutboundOptions, dialer mieruDialer) (*mieruclient.ClientConfig, error) {
if err := validateMieruOptions(options); err != nil {
return nil, fmt.Errorf("failed to validate mieru options: %w", err)
}
transportProtocol := mierupb.TransportProtocol_TCP.Enum()
server := &mierupb.ServerEndpoint{}
if options.ServerPort != 0 {
server.PortBindings = append(server.PortBindings, &mierupb.PortBinding{
Port: proto.Int32(int32(options.ServerPort)),
Protocol: transportProtocol,
})
}
for _, pr := range options.ServerPortRanges {
server.PortBindings = append(server.PortBindings, &mierupb.PortBinding{
PortRange: proto.String(pr),
Protocol: transportProtocol,
})
}
if M.IsDomainName(options.Server) {
server.DomainName = proto.String(options.Server)
} else {
server.IpAddress = proto.String(options.Server)
}
config := &mieruclient.ClientConfig{
Profile: &mierupb.ClientProfile{
ProfileName: proto.String("sing-box"),
User: &mierupb.User{
Name: proto.String(options.UserName),
Password: proto.String(options.Password),
},
Servers: []*mierupb.ServerEndpoint{server},
},
Dialer: dialer,
}
if multiplexing, ok := mierupb.MultiplexingLevel_value[options.Multiplexing]; ok {
config.Profile.Multiplexing = &mierupb.MultiplexingConfig{
Level: mierupb.MultiplexingLevel(multiplexing).Enum(),
}
}
return config, nil
}
func validateMieruOptions(options option.MieruOutboundOptions) error {
if options.Server == "" {
return fmt.Errorf("server is empty")
}
if options.ServerPort == 0 && len(options.ServerPortRanges) == 0 {
return fmt.Errorf("either server_port or server_ports must be set")
}
for _, pr := range options.ServerPortRanges {
begin, end, err := beginAndEndPortFromPortRange(pr)
if err != nil {
return fmt.Errorf("invalid server_ports format")
}
if begin < 1 || begin > 65535 {
return fmt.Errorf("begin port must be between 1 and 65535")
}
if end < 1 || end > 65535 {
return fmt.Errorf("end port must be between 1 and 65535")
}
if begin > end {
return fmt.Errorf("begin port must be less than or equal to end port")
}
}
if options.Transport != "TCP" {
return fmt.Errorf("transport must be TCP")
}
if options.UserName == "" {
return fmt.Errorf("username is empty")
}
if options.Password == "" {
return fmt.Errorf("password is empty")
}
if options.Multiplexing != "" {
if _, ok := mierupb.MultiplexingLevel_value[options.Multiplexing]; !ok {
return fmt.Errorf("invalid multiplexing level: %s", options.Multiplexing)
}
}
return nil
}
func beginAndEndPortFromPortRange(portRange string) (int, int, error) {
var begin, end int
_, err := fmt.Sscanf(portRange, "%d-%d", &begin, &end)
return begin, end, err
}

151
protocol/tunnel/client.go Normal file
View 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) {})
}

View 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
View 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
View 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")
}

View File

@@ -185,31 +185,28 @@ func (w *WARPEndpoint) Start(stage adapter.StartStage) error {
}
func (w *WARPEndpoint) Close() error {
if err := w.isEndpointInitialized(); err != nil {
return err
if ok := w.isEndpointInitialized(); !ok {
return E.New("endpoint not initialized")
}
return w.endpoint.Close()
}
func (w *WARPEndpoint) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
if err := w.isEndpointInitialized(); err != nil {
return nil, err
if ok := w.isEndpointInitialized(); !ok {
return nil, E.New("endpoint not initialized")
}
return w.endpoint.DialContext(ctx, network, destination)
}
func (w *WARPEndpoint) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
if err := w.isEndpointInitialized(); err != nil {
return nil, err
if ok := w.isEndpointInitialized(); !ok {
return nil, E.New("endpoint not initialized")
}
return w.endpoint.ListenPacket(ctx, destination)
}
func (w *WARPEndpoint) isEndpointInitialized() error {
func (w *WARPEndpoint) isEndpointInitialized() bool {
w.mtx.Lock()
defer w.mtx.Unlock()
if w.endpoint == nil {
return E.New("endpoint not initialized")
}
return nil
return w.endpoint != nil
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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))
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View 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, " "), "]")
}
}

View 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, " "), "]")
}
}

View File

@@ -69,7 +69,7 @@ func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, opt
return requestURL
}
if dest.IsFqdn() {
addresses, err := router.Lookup(ctx, dest.AddrString(), dns.DomainStrategy(options.DomainStrategy))
addresses, err := router.Lookup(ctx, dest.Fqdn, dns.DomainStrategy(options.DomainStrategy))
if err != nil {
return nil, err
}
@@ -123,7 +123,7 @@ func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, opt
return requestURL2
}
if dest2.IsFqdn() {
addresses2, err := router.Lookup(ctx, dest2.AddrString(), dns.DomainStrategy(options2.DomainStrategy))
addresses2, err := router.Lookup(ctx, dest2.Fqdn, dns.DomainStrategy(options2.DomainStrategy))
if err != nil {
return nil, err
}