Compare commits

..

31 Commits

Author SHA1 Message Date
Sergei Maklagin
89497dbfd5 Update dependencies 2025-07-13 21:48:31 +03:00
Sergei Maklagin
8388abbb77 Resolve conflicts 2025-07-13 21:44:23 +03:00
Sergei Maklagin
180b7c4134 Fix tunnel client 2025-07-13 21:41:41 +03:00
Sergei Maklagin
deda2cca5e Fix xhttp transport 2025-07-13 21:40:34 +03:00
世界
bc35aca017 Bump version 2025-07-08 13:11:13 +08:00
世界
281d52a1ea Fix hy2 server crash 2025-07-08 13:11:13 +08:00
世界
b8502759b5 Fix DNS reject check 2025-07-07 13:57:37 +08:00
Sergei Maklagin
209b89a4a3 Add tunnel 2025-07-06 18:31:06 +03:00
世界
6f804adf39 Fix v2rayhttp crash 2025-07-03 21:48:10 +08:00
Kyson
36db31c55a documentation: Fix typo
Co-authored-by: chenqixin <chenqixin@bytedance.com>
2025-06-29 18:54:05 +08:00
世界
4dbbf59c82 Fix logger for acme 2025-06-29 18:44:40 +08:00
世界
832eb4458d release: Fix xcode version 2025-06-29 18:44:40 +08:00
dyhkwong
2cf989d306 Fix inbound with v2ray transport missing InboundOptions 2025-06-25 13:20:00 +08:00
世界
7d3ee29bd0 Also skip duplicate sniff for TCP 2025-06-21 12:57:27 +08:00
世界
cba0e46aba Fix log for rejected connections 2025-06-21 12:57:26 +08: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
74 changed files with 2396 additions and 369 deletions

View File

@@ -468,11 +468,11 @@ jobs:
- name: Setup Xcode stable
if: matrix.if && github.ref == 'refs/heads/main-next'
run: |-
sudo xcode-select -s /Applications/Xcode_16.2.app
sudo xcode-select -s /Applications/Xcode_16.4.app
- name: Setup Xcode beta
if: matrix.if && github.ref == 'refs/heads/dev-next'
run: |-
sudo xcode-select -s /Applications/Xcode_16.2.app
sudo xcode-select -s /Applications/Xcode_16.4.app
- name: Set tag
if: matrix.if
run: |-

View File

@@ -1,6 +1,6 @@
NAME = sing-box
COMMIT = $(shell git rev-parse --short HEAD)
TAGS_GO120 = with_gvisor,with_dhcp,with_wireguard,with_reality_server,with_clash_api,with_quic,with_utls
TAGS_GO120 = with_gvisor,with_dhcp,with_wireguard,with_reality_server,with_clash_api,with_quic,with_utls,with_acme
TAGS_GO121 = with_ech
TAGS ?= $(TAGS_GO118),$(TAGS_GO120),$(TAGS_GO121)
TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_ech,with_utls,with_reality_server

View File

@@ -42,22 +42,24 @@ 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
Protocol string
Domain string
Client string
SniffContext any
PacketSniffError error
Protocol string
Domain string
Client string
SniffContext any
SniffError error
// cache

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

View File

@@ -5,13 +5,13 @@ package tls
import (
"context"
"crypto/tls"
"os"
"strings"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
"github.com/caddyserver/certmagic"
"github.com/libdns/alidns"
@@ -37,7 +37,38 @@ func (w *acmeWrapper) Close() error {
return nil
}
func startACME(ctx context.Context, options option.InboundACMEOptions) (*tls.Config, adapter.Service, error) {
type acmeLogWriter struct {
logger logger.Logger
}
func (w *acmeLogWriter) Write(p []byte) (n int, err error) {
logLine := strings.ReplaceAll(string(p), " ", ": ")
switch {
case strings.HasPrefix(logLine, "error: "):
w.logger.Error(logLine[7:])
case strings.HasPrefix(logLine, "warn: "):
w.logger.Warn(logLine[6:])
case strings.HasPrefix(logLine, "info: "):
w.logger.Info(logLine[6:])
case strings.HasPrefix(logLine, "debug: "):
w.logger.Debug(logLine[7:])
default:
w.logger.Debug(logLine)
}
return len(p), nil
}
func (w *acmeLogWriter) Sync() error {
return nil
}
func encoderConfig() zapcore.EncoderConfig {
config := zap.NewProductionEncoderConfig()
config.TimeKey = zapcore.OmitKey
return config
}
func startACME(ctx context.Context, logger logger.Logger, options option.InboundACMEOptions) (*tls.Config, adapter.Service, error) {
var acmeServer string
switch options.Provider {
case "", "letsencrypt":
@@ -58,14 +89,15 @@ func startACME(ctx context.Context, options option.InboundACMEOptions) (*tls.Con
} else {
storage = certmagic.Default.Storage
}
zapLogger := zap.New(zapcore.NewCore(
zapcore.NewConsoleEncoder(encoderConfig()),
&acmeLogWriter{logger: logger},
zap.DebugLevel,
))
config := &certmagic.Config{
DefaultServerName: options.DefaultServerName,
Storage: storage,
Logger: zap.New(zapcore.NewCore(
zapcore.NewConsoleEncoder(zap.NewProductionEncoderConfig()),
os.Stderr,
zap.InfoLevel,
)),
Logger: zapLogger,
}
acmeConfig := certmagic.ACMEIssuer{
CA: acmeServer,
@@ -75,7 +107,7 @@ func startACME(ctx context.Context, options option.InboundACMEOptions) (*tls.Con
DisableTLSALPNChallenge: options.DisableTLSALPNChallenge,
AltHTTPPort: int(options.AlternativeHTTPPort),
AltTLSALPNPort: int(options.AlternativeTLSPort),
Logger: config.Logger,
Logger: zapLogger,
}
if dnsOptions := options.DNS01Challenge; dnsOptions != nil && dnsOptions.Provider != "" {
var solver certmagic.DNS01Solver
@@ -103,6 +135,7 @@ func startACME(ctx context.Context, options option.InboundACMEOptions) (*tls.Con
GetConfigForCert: func(certificate certmagic.Certificate) (*certmagic.Config, error) {
return config, nil
},
Logger: zapLogger,
})
config = certmagic.New(cache, *config)
var tlsConfig *tls.Config

View File

@@ -9,8 +9,9 @@ import (
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
)
func startACME(ctx context.Context, options option.InboundACMEOptions) (*tls.Config, adapter.Service, error) {
func startACME(ctx context.Context, logger logger.Logger, options option.InboundACMEOptions) (*tls.Config, adapter.Service, error) {
return nil, nil, E.New(`ACME is not included in this build, rebuild with -tags with_acme`)
}

View File

@@ -157,7 +157,7 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound
var err error
if options.ACME != nil && len(options.ACME.Domain) > 0 {
//nolint:staticcheck
tlsConfig, acmeService, err = startACME(ctx, common.PtrValueOrDefault(options.ACME))
tlsConfig, acmeService, err = startACME(ctx, logger, common.PtrValueOrDefault(options.ACME))
if err != nil {
return nil, err
}

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,20 @@
icon: material/alert-decagram
---
### 1.11.15
* 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.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

@@ -63,7 +63,7 @@ icon: material/new-box
{
"external_controller": "0.0.0.0:9090",
"external_ui": "dashboard"
// external_ui_download_detour: "direct"
// "external_ui_download_detour": "direct"
}
```

View File

@@ -63,7 +63,7 @@ icon: material/new-box
{
"external_controller": "0.0.0.0:9090",
"external_ui": "dashboard"
// external_ui_download_detour: "direct"
// "external_ui_download_detour": "direct"
}
```

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,

22
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
@@ -30,13 +31,13 @@ require (
github.com/sagernet/quic-go v0.49.0-beta.1
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691
github.com/sagernet/sing v0.6.10
github.com/sagernet/sing-dns v0.4.5
github.com/sagernet/sing-dns v0.4.6
github.com/sagernet/sing-mux v0.3.2
github.com/sagernet/sing-quic v0.4.3
github.com/sagernet/sing-quic v0.4.4
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.9
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.6-extended-1.0.0
replace github.com/ameshkov/dnscrypt/v2 => github.com/shtorm-7/dnscrypt/v2 v2.4.0-extended-1.0.0

48
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,20 +139,18 @@ 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=
github.com/sagernet/sing-quic v0.4.3/go.mod h1:tqPa0/Wqa19MkkSlKVZZX5sHxtiDR9BROcn4ufcbVdY=
github.com/sagernet/sing-quic v0.4.4 h1:qqOCLnzHbqKkj/wBcXEI3rhSyqoGlqDdv2S6mz2d/JA=
github.com/sagernet/sing-quic v0.4.4/go.mod h1:tqPa0/Wqa19MkkSlKVZZX5sHxtiDR9BROcn4ufcbVdY=
github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE=
github.com/sagernet/sing-shadowsocks v0.2.8/go.mod h1:lo7TWEMDcN5/h5B8S0ew+r78ZODn6SwVaFhvB6H+PTI=
github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnqqs2gQ2/Qioo=
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.9 h1:uP8O4Q7U9QesjWumgxd2S9fjT3c6aEPWl5RB6uBdVB8=
github.com/sagernet/sing-tun v0.6.9/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.6-extended-1.0.0 h1:/fyqTYciGYTEbDDySW22t9vY5bSr+lCeLZfjVlv9qgw=
github.com/shtorm-7/sing-dns v0.4.6-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
}

152
protocol/tunnel/client.go Normal file
View File

@@ -0,0 +1,152 @@
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"
"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 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 common.Close(c.outbound)
}
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

@@ -205,6 +205,10 @@ func (h *inboundTransportHandler) NewConnectionEx(ctx context.Context, conn net.
var metadata adapter.InboundContext
metadata.Source = source
metadata.Destination = destination
//nolint:staticcheck
metadata.InboundDetour = h.listener.ListenOptions().Detour
//nolint:staticcheck
metadata.InboundOptions = h.listener.ListenOptions().InboundOptions
h.logger.InfoContext(ctx, "inbound connection from ", metadata.Source)
(*Inbound)(h).NewConnectionEx(ctx, conn, metadata, onClose)
}

View File

@@ -219,6 +219,10 @@ func (h *inboundTransportHandler) NewConnectionEx(ctx context.Context, conn net.
var metadata adapter.InboundContext
metadata.Source = source
metadata.Destination = destination
//nolint:staticcheck
metadata.InboundDetour = h.listener.ListenOptions().Detour
//nolint:staticcheck
metadata.InboundOptions = h.listener.ListenOptions().InboundOptions
h.logger.InfoContext(ctx, "inbound connection from ", metadata.Source)
(*Inbound)(h).NewConnectionEx(ctx, conn, metadata, onClose)
}

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

@@ -2,15 +2,14 @@ package route
import (
"context"
"errors"
"net"
"time"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
dnsOutbound "github.com/sagernet/sing-box/protocol/dns"
R "github.com/sagernet/sing-box/route/rule"
"github.com/sagernet/sing-dns"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common/buf"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
@@ -31,7 +30,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 +44,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"))
}
@@ -56,7 +57,7 @@ func (r *Router) hijackDNSPacket(ctx context.Context, conn N.PacketConn, packetB
func ExchangeDNSPacket(ctx context.Context, router *Router, conn N.PacketConn, buffer *buf.Buffer, metadata adapter.InboundContext, destination M.Socksaddr) {
err := exchangeDNSPacket(ctx, router, conn, buffer, metadata, destination)
if err != nil && !errors.Is(err, tun.ErrDrop) && !E.IsClosedOrCanceled(err) {
if err != nil && !R.IsRejected(err) && !E.IsClosedOrCanceled(err) {
router.dnsLogger.ErrorContext(ctx, E.Cause(err, "process packet connection"))
}
}
@@ -85,8 +86,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

@@ -16,7 +16,7 @@ import (
"github.com/sagernet/sing-box/common/sniff"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/route/rule"
R "github.com/sagernet/sing-box/route/rule"
"github.com/sagernet/sing-dns"
"github.com/sagernet/sing-mux"
"github.com/sagernet/sing-vmess"
@@ -51,7 +51,7 @@ func (r *Router) RouteConnectionEx(ctx context.Context, conn net.Conn, metadata
err := r.routeConnection(ctx, conn, metadata, onClose)
if err != nil {
N.CloseOnHandshakeFailure(conn, onClose, err)
if E.IsClosedOrCanceled(err) {
if E.IsClosedOrCanceled(err) || R.IsRejected(err) {
r.logger.DebugContext(ctx, "connection closed: ", err)
} else {
r.logger.ErrorContext(ctx, err)
@@ -101,7 +101,7 @@ func (r *Router) routeConnection(ctx context.Context, conn net.Conn, metadata ad
var selectedOutbound adapter.Outbound
if selectedRule != nil {
switch action := selectedRule.Action().(type) {
case *rule.RuleActionRoute:
case *R.RuleActionRoute:
var loaded bool
selectedOutbound, loaded = r.outbound.Outbound(action.Outbound)
if !loaded {
@@ -112,15 +112,15 @@ func (r *Router) routeConnection(ctx context.Context, conn net.Conn, metadata ad
buf.ReleaseMulti(buffers)
return E.New("TCP is not supported by outbound: ", selectedOutbound.Tag())
}
case *rule.RuleActionReject:
case *R.RuleActionReject:
buf.ReleaseMulti(buffers)
N.CloseOnHandshakeFailure(conn, onClose, action.Error(ctx))
return nil
case *rule.RuleActionHijackDNS:
case *R.RuleActionHijackDNS:
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
}
}
@@ -154,7 +154,7 @@ func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, m
}))
if err != nil {
conn.Close()
if E.IsClosedOrCanceled(err) {
if E.IsClosedOrCanceled(err) || R.IsRejected(err) {
r.logger.DebugContext(ctx, "connection closed: ", err)
} else {
r.logger.ErrorContext(ctx, err)
@@ -171,7 +171,7 @@ func (r *Router) RoutePacketConnectionEx(ctx context.Context, conn N.PacketConn,
err := r.routePacketConnection(ctx, conn, metadata, onClose)
if err != nil {
N.CloseOnHandshakeFailure(conn, onClose, err)
if E.IsClosedOrCanceled(err) {
if E.IsClosedOrCanceled(err) || R.IsRejected(err) {
r.logger.DebugContext(ctx, "connection closed: ", err)
} else {
r.logger.ErrorContext(ctx, err)
@@ -217,7 +217,7 @@ func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, m
var selectReturn bool
if selectedRule != nil {
switch action := selectedRule.Action().(type) {
case *rule.RuleActionRoute:
case *R.RuleActionRoute:
var loaded bool
selectedOutbound, loaded = r.outbound.Outbound(action.Outbound)
if !loaded {
@@ -228,12 +228,12 @@ func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, m
N.ReleaseMultiPacketBuffer(packetBuffers)
return E.New("UDP is not supported by outbound: ", selectedOutbound.Tag())
}
case *rule.RuleActionReject:
case *R.RuleActionReject:
N.ReleaseMultiPacketBuffer(packetBuffers)
N.CloseOnHandshakeFailure(conn, onClose, action.Error(ctx))
return nil
case *rule.RuleActionHijackDNS:
r.hijackDNSPacket(ctx, conn, packetBuffers, metadata)
case *R.RuleActionHijackDNS:
r.hijackDNSPacket(ctx, conn, packetBuffers, metadata, onClose)
return nil
}
}
@@ -271,7 +271,7 @@ func (r *Router) PreMatch(metadata adapter.InboundContext) error {
if selectedRule == nil {
return nil
}
rejectAction, isReject := selectedRule.Action().(*rule.RuleActionReject)
rejectAction, isReject := selectedRule.Action().(*R.RuleActionReject)
if !isReject {
return nil
}
@@ -346,7 +346,7 @@ func (r *Router) matchRule(
//nolint:staticcheck
if metadata.InboundOptions != common.DefaultValue[option.InboundOptions]() {
if !preMatch && metadata.InboundOptions.SniffEnabled {
newBuffer, newPackerBuffers, newErr := r.actionSniff(ctx, metadata, &rule.RuleActionSniff{
newBuffer, newPackerBuffers, newErr := r.actionSniff(ctx, metadata, &R.RuleActionSniff{
OverrideDestination: metadata.InboundOptions.SniffOverrideDestination,
Timeout: time.Duration(metadata.InboundOptions.SniffTimeout),
}, inputConn, inputPacketConn, nil)
@@ -361,7 +361,7 @@ func (r *Router) matchRule(
}
}
if dns.DomainStrategy(metadata.InboundOptions.DomainStrategy) != dns.DomainStrategyAsIS {
fatalErr = r.actionResolve(ctx, metadata, &rule.RuleActionResolve{
fatalErr = r.actionResolve(ctx, metadata, &R.RuleActionResolve{
Strategy: dns.DomainStrategy(metadata.InboundOptions.DomainStrategy),
})
if fatalErr != nil {
@@ -398,11 +398,11 @@ match:
}
}
}
var routeOptions *rule.RuleActionRouteOptions
var routeOptions *R.RuleActionRouteOptions
switch action := currentRule.Action().(type) {
case *rule.RuleActionRoute:
case *R.RuleActionRoute:
routeOptions = &action.RuleActionRouteOptions
case *rule.RuleActionRouteOptions:
case *R.RuleActionRouteOptions:
routeOptions = action
}
if routeOptions != 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
}
@@ -448,7 +451,7 @@ match:
}
}
switch action := currentRule.Action().(type) {
case *rule.RuleActionSniff:
case *R.RuleActionSniff:
if !preMatch {
newBuffer, newPacketBuffers, newErr := r.actionSniff(ctx, metadata, action, inputConn, inputPacketConn, buffers)
if newErr != nil {
@@ -465,7 +468,7 @@ match:
selectedRuleIndex = currentRuleIndex
break match
}
case *rule.RuleActionResolve:
case *R.RuleActionResolve:
fatalErr = r.actionResolve(ctx, metadata, action)
if fatalErr != nil {
return
@@ -485,7 +488,7 @@ match:
}
func (r *Router) actionSniff(
ctx context.Context, metadata *adapter.InboundContext, action *rule.RuleActionSniff,
ctx context.Context, metadata *adapter.InboundContext, action *R.RuleActionSniff,
inputConn net.Conn, inputPacketConn N.PacketConn, inputBuffers []*buf.Buffer,
) (buffer *buf.Buffer, packetBuffers []*N.PacketBuffer, fatalErr error) {
if sniff.Skip(metadata) {
@@ -498,6 +501,9 @@ func (r *Router) actionSniff(
if inputConn != nil {
if len(action.StreamSniffers) == 0 && len(action.PacketSniffers) > 0 {
return
} else if metadata.SniffError != nil && !errors.Is(metadata.SniffError, sniff.ErrNeedMoreData) {
r.logger.DebugContext(ctx, "packet sniff skipped due to previous error: ", metadata.SniffError)
return
}
var streamSniffers []sniff.StreamSniffer
if len(action.StreamSniffers) > 0 {
@@ -522,6 +528,7 @@ func (r *Router) actionSniff(
action.Timeout,
streamSniffers...,
)
metadata.SniffError = err
if err == nil {
//goland:noinspection GoDeprecation
if action.OverrideDestination && M.IsDomainName(metadata.Domain) {
@@ -546,8 +553,8 @@ func (r *Router) actionSniff(
} else if inputPacketConn != nil {
if len(action.PacketSniffers) == 0 && len(action.StreamSniffers) > 0 {
return
} else if metadata.PacketSniffError != nil && !errors.Is(metadata.PacketSniffError, sniff.ErrNeedMoreData) {
r.logger.DebugContext(ctx, "packet sniff skipped due to previous error: ", metadata.PacketSniffError)
} else if metadata.SniffError != nil && !errors.Is(metadata.SniffError, sniff.ErrNeedMoreData) {
r.logger.DebugContext(ctx, "packet sniff skipped due to previous error: ", metadata.SniffError)
return
}
var packetSniffers []sniff.PacketSniffer
@@ -594,7 +601,7 @@ func (r *Router) actionSniff(
return
}
} else {
if len(packetBuffers) > 0 || metadata.PacketSniffError != nil {
if len(packetBuffers) > 0 || metadata.SniffError != nil {
err = sniff.PeekPacket(
ctx,
metadata,
@@ -614,7 +621,7 @@ func (r *Router) actionSniff(
Destination: destination,
}
packetBuffers = append(packetBuffers, packetBuffer)
metadata.PacketSniffError = err
metadata.SniffError = err
if errors.Is(err, sniff.ErrNeedMoreData) {
// TODO: replace with generic message when there are more multi-packet protocols
r.logger.DebugContext(ctx, "attempt to sniff fragmented QUIC client hello")
@@ -645,7 +652,7 @@ func (r *Router) actionSniff(
return
}
func (r *Router) actionResolve(ctx context.Context, metadata *adapter.InboundContext, action *rule.RuleActionResolve) error {
func (r *Router) actionResolve(ctx context.Context, metadata *adapter.InboundContext, action *R.RuleActionResolve) error {
if metadata.Destination.IsFqdn() {
metadata.DNSServer = action.Server
addresses, err := r.Lookup(adapter.WithContext(ctx, metadata), metadata.Destination.Fqdn, action.Strategy)

View File

@@ -170,7 +170,7 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, er
Question: []mDNS.Question{message.Question[0]},
}, nil
case C.RuleActionRejectMethodDrop:
return nil, tun.ErrDrop
return nil, &R.RejectedError{Cause: tun.ErrDrop}
}
}
}
@@ -289,7 +289,7 @@ func (r *Router) Lookup(ctx context.Context, domain string, strategy dns.DomainS
case C.RuleActionRejectMethodDefault:
return nil, nil
case C.RuleActionRejectMethodDrop:
return nil, tun.ErrDrop
return nil, &R.RejectedError{Cause: tun.ErrDrop}
}
}
}

View File

@@ -2,6 +2,7 @@ package rule
import (
"context"
"errors"
"net/netip"
"strings"
"sync"
@@ -33,6 +34,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 +149,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 +171,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))
}
@@ -250,6 +256,23 @@ func (r *RuleActionDirect) String() string {
return "direct" + r.description
}
type RejectedError struct {
Cause error
}
func (r *RejectedError) Error() string {
return "rejected"
}
func (r *RejectedError) Unwrap() error {
return r.Cause
}
func IsRejected(err error) bool {
var rejected *RejectedError
return errors.As(err, &rejected)
}
type RuleActionReject struct {
Method string
NoDrop bool
@@ -273,9 +296,9 @@ func (r *RuleActionReject) Error(ctx context.Context) error {
var returnErr error
switch r.Method {
case C.RuleActionRejectMethodDefault:
returnErr = syscall.ECONNREFUSED
returnErr = &RejectedError{syscall.ECONNREFUSED}
case C.RuleActionRejectMethodDrop:
return tun.ErrDrop
return &RejectedError{tun.ErrDrop}
default:
panic(F.ToString("unknown reject method: ", r.Method))
}
@@ -293,7 +316,7 @@ func (r *RuleActionReject) Error(ctx context.Context) error {
if ctx != nil {
r.logger.DebugContext(ctx, "dropped due to flooding")
}
return tun.ErrDrop
return &RejectedError{tun.ErrDrop}
}
return returnErr
}

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

@@ -31,6 +31,9 @@ type HTTPConn struct {
}
func NewHTTP1Conn(conn net.Conn, request *http.Request) *HTTPConn {
if request.Header.Get("Host") == "" {
request.Header.Set("Host", request.Host)
}
return &HTTPConn{
Conn: conn,
request: request,
@@ -89,9 +92,6 @@ func (c *HTTPConn) writeRequest(payload []byte) error {
if err != nil {
return err
}
if c.request.Header.Get("Host") == "" {
c.request.Header.Set("Host", c.request.Host)
}
for key, value := range c.request.Header {
_, err = writer.Write([]byte(F.ToString(key, ": ", strings.Join(value, ", "), CRLF)))
if err != nil {

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
}
@@ -87,9 +87,7 @@ func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, opt
return xmuxClient.XmuxConn.(DialerClient), xmuxClient
}
getRequestURL2 := getRequestURL
getHTTPClient2 := func() (DialerClient, *XmuxClient) {
return nil, nil
}
getHTTPClient2 := getHTTPClient
if options.Download != nil {
options2 := options.Download
dialer2 := dialer
@@ -123,7 +121,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
}