Compare commits

..

46 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
Sergei Maklagin
6768c77fa0 Merge tag 'v1.11.13' into HEAD 2025-06-08 22:43:21 +03:00
世界
43a9016c83 Fix leak in hijack-dns 2025-06-06 14:28:09 +08:00
世界
255068fd40 Bump version 2025-06-04 23:32:10 +08:00
世界
098a00b025 Fix v2ray websocket transport 2025-06-04 23:23:36 +08:00
世界
dba0b5276b Bump version 2025-06-04 20:06:38 +08:00
Sentsuki
78ae935468 documentation: Fix typo
Signed-off-by: Sentsuki <52487960+Sentsuki@users.noreply.github.com>
2025-06-04 20:06:38 +08:00
Mahdi
3ea5f76470 Fix nil logger at v2rayhttp server 2025-06-04 20:06:20 +08:00
世界
b4d294c05e Fix TUIC read buffer 2025-06-04 20:03:51 +08:00
世界
83cf5f5c6a Fix ws closed error message 2025-05-27 14:30:07 +08:00
世界
e7b3a8eebe Fix vmess read request 2025-05-27 14:11:05 +08:00
世界
ee3a42a67e Fix none method read buffer 2025-05-27 14:03:48 +08:00
世界
50227c0f5f Fix sniff action 2025-05-26 18:24:35 +08:00
世界
bc5eb1e1a5 Fix RoutePacketConnectionEx 2025-05-24 08:14:43 +08:00
世界
995267a042 Remove wrong ALPNs in DOH/DOH3 2025-05-24 08:00:13 +08:00
世界
41226a6075 Fix interface finder 2025-05-23 10:57:38 +08:00
世界
81d32181ce Fix update route address set 2025-05-20 19:46:54 +08:00
80 changed files with 2474 additions and 437 deletions

View File

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

View File

@@ -1,6 +1,6 @@
NAME = sing-box NAME = sing-box
COMMIT = $(shell git rev-parse --short HEAD) 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_GO121 = with_ech
TAGS ?= $(TAGS_GO118),$(TAGS_GO120),$(TAGS_GO121) TAGS ?= $(TAGS_GO118),$(TAGS_GO120),$(TAGS_GO121)
TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_ech,with_utls,with_reality_server TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_ech,with_utls,with_reality_server

View File

@@ -48,6 +48,8 @@ type InboundContext struct {
Network string Network string
Source M.Socksaddr Source M.Socksaddr
Destination M.Socksaddr Destination M.Socksaddr
TunnelSource string
TunnelDestination string
User string User string
Outbound string Outbound string
@@ -57,7 +59,7 @@ type InboundContext struct {
Domain string Domain string
Client string Client string
SniffContext any SniffContext any
PacketSniffError error SniffError error
// cache // cache

4
box.go
View File

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

View File

@@ -100,10 +100,6 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
} else if networkManager.AutoDetectInterface() { } else if networkManager.AutoDetectInterface() {
if platformInterface != nil { if platformInterface != nil {
networkStrategy = (*C.NetworkStrategy)(options.NetworkStrategy) networkStrategy = (*C.NetworkStrategy)(options.NetworkStrategy)
if networkStrategy == nil {
networkStrategy = common.Ptr(C.NetworkStrategyDefault)
defaultNetworkStrategy = true
}
networkType = common.Map(options.NetworkType, option.InterfaceType.Build) networkType = common.Map(options.NetworkType, option.InterfaceType.Build)
fallbackNetworkType = common.Map(options.FallbackNetworkType, option.InterfaceType.Build) fallbackNetworkType = common.Map(options.FallbackNetworkType, option.InterfaceType.Build)
if networkStrategy == nil && len(networkType) == 0 && len(fallbackNetworkType) == 0 { 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 { if networkFallbackDelay == 0 && defaultOptions.FallbackDelay != 0 {
networkFallbackDelay = defaultOptions.FallbackDelay networkFallbackDelay = defaultOptions.FallbackDelay
} }
if networkStrategy == nil {
networkStrategy = common.Ptr(C.NetworkStrategyDefault)
defaultNetworkStrategy = true
}
bindFunc := networkManager.ProtectFunc() bindFunc := networkManager.ProtectFunc()
dialer.Control = control.Append(dialer.Control, bindFunc) dialer.Control = control.Append(dialer.Control, bindFunc)
listener.Control = control.Append(listener.Control, bindFunc) listener.Control = control.Append(listener.Control, bindFunc)

View File

@@ -10,9 +10,7 @@ import (
"sync" "sync"
"time" "time"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/bufio" "github.com/sagernet/sing/common/bufio"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
@@ -26,7 +24,9 @@ type slowOpenConn struct {
destination M.Socksaddr destination M.Socksaddr
conn net.Conn conn net.Conn
create chan struct{} create chan struct{}
done chan struct{}
access sync.Mutex access sync.Mutex
closeOnce sync.Once
err error err error
} }
@@ -45,6 +45,7 @@ func DialSlowContext(dialer *tcpDialer, ctx context.Context, network string, des
network: network, network: network,
destination: destination, destination: destination,
create: make(chan struct{}), create: make(chan struct{}),
done: make(chan struct{}),
}, nil }, nil
} }
@@ -55,8 +56,8 @@ func (c *slowOpenConn) Read(b []byte) (n int, err error) {
if c.err != nil { if c.err != nil {
return 0, c.err return 0, c.err
} }
case <-c.ctx.Done(): case <-c.done:
return 0, c.ctx.Err() return 0, os.ErrClosed
} }
} }
return c.conn.Read(b) return c.conn.Read(b)
@@ -74,12 +75,15 @@ func (c *slowOpenConn) Write(b []byte) (n int, err error) {
return 0, c.err return 0, c.err
} }
return c.conn.Write(b) return c.conn.Write(b)
case <-c.done:
return 0, os.ErrClosed
default: 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 { if err != nil {
c.conn = nil c.err = err
c.err = E.Cause(err, "dial tcp fast open") } else {
c.conn = conn
} }
n = len(b) n = len(b)
close(c.create) close(c.create)
@@ -87,7 +91,13 @@ func (c *slowOpenConn) Write(b []byte) (n int, err error) {
} }
func (c *slowOpenConn) Close() 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 { 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 { if c.err != nil {
return 0, c.err return 0, c.err
} }
case <-c.ctx.Done(): case <-c.done:
return 0, c.ctx.Err() return 0, c.err
} }
} }
return bufio.Copy(w, c.conn) return bufio.Copy(w, c.conn)

View File

@@ -3,6 +3,7 @@ package interrupt
import ( import (
"net" "net"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/x/list" "github.com/sagernet/sing/common/x/list"
) )
@@ -73,3 +74,32 @@ func (c *PacketConn) WriterReplaceable() bool {
func (c *PacketConn) Upstream() any { func (c *PacketConn) Upstream() any {
return c.PacketConn 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" "net"
"sync" "sync"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/x/list" "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} 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) { func (g *Group) Interrupt(interruptExternalConnections bool) {
g.access.Lock() g.access.Lock()
defer g.access.Unlock() 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)) // rup8(sizeof(xtcpcb_n))
itemSize += 208 itemSize += 208
} }
var fallbackUDPProcess string
// skip the first xinpgen(24 bytes) block // skip the first xinpgen(24 bytes) block
for i := 24; i+itemSize <= len(buf); i += itemSize { for i := 24; i+itemSize <= len(buf); i += itemSize {
// offset of xinpcb_n and xsocket_n // 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] flag := buf[inp+44]
var srcIP netip.Addr var srcIP netip.Addr
srcIsIPv4 := false
switch { switch {
case flag&0x1 > 0 && isIPv4: case flag&0x1 > 0 && isIPv4:
// ipv4 // ipv4
srcIP = netip.AddrFrom4(*(*[4]byte)(buf[inp+76 : inp+80])) srcIP = netip.AddrFrom4(*(*[4]byte)(buf[inp+76 : inp+80]))
srcIsIPv4 = true
case flag&0x2 > 0 && !isIPv4: case flag&0x2 > 0 && !isIPv4:
// ipv6 // ipv6
srcIP = netip.AddrFrom16(*(*[16]byte)(buf[inp+64 : inp+80])) srcIP = netip.AddrFrom16(*(*[16]byte)(buf[inp+64 : inp+80]))
@@ -101,15 +105,23 @@ func findProcessName(network string, ip netip.Addr, port int) (string, error) {
continue continue
} }
if ip != srcIP { if ip == srcIP {
continue
}
// xsocket_n.so_last_pid // xsocket_n.so_last_pid
pid := readNativeUint32(buf[so+68 : so+72]) pid := readNativeUint32(buf[so+68 : so+72])
return getExecPathFromPID(pid) 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 return "", ErrNotFound
} }

View File

@@ -5,13 +5,13 @@ package tls
import ( import (
"context" "context"
"crypto/tls" "crypto/tls"
"os"
"strings" "strings"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
"github.com/caddyserver/certmagic" "github.com/caddyserver/certmagic"
"github.com/libdns/alidns" "github.com/libdns/alidns"
@@ -37,7 +37,38 @@ func (w *acmeWrapper) Close() error {
return nil 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 var acmeServer string
switch options.Provider { switch options.Provider {
case "", "letsencrypt": case "", "letsencrypt":
@@ -58,14 +89,15 @@ func startACME(ctx context.Context, options option.InboundACMEOptions) (*tls.Con
} else { } else {
storage = certmagic.Default.Storage storage = certmagic.Default.Storage
} }
zapLogger := zap.New(zapcore.NewCore(
zapcore.NewConsoleEncoder(encoderConfig()),
&acmeLogWriter{logger: logger},
zap.DebugLevel,
))
config := &certmagic.Config{ config := &certmagic.Config{
DefaultServerName: options.DefaultServerName, DefaultServerName: options.DefaultServerName,
Storage: storage, Storage: storage,
Logger: zap.New(zapcore.NewCore( Logger: zapLogger,
zapcore.NewConsoleEncoder(zap.NewProductionEncoderConfig()),
os.Stderr,
zap.InfoLevel,
)),
} }
acmeConfig := certmagic.ACMEIssuer{ acmeConfig := certmagic.ACMEIssuer{
CA: acmeServer, CA: acmeServer,
@@ -75,7 +107,7 @@ func startACME(ctx context.Context, options option.InboundACMEOptions) (*tls.Con
DisableTLSALPNChallenge: options.DisableTLSALPNChallenge, DisableTLSALPNChallenge: options.DisableTLSALPNChallenge,
AltHTTPPort: int(options.AlternativeHTTPPort), AltHTTPPort: int(options.AlternativeHTTPPort),
AltTLSALPNPort: int(options.AlternativeTLSPort), AltTLSALPNPort: int(options.AlternativeTLSPort),
Logger: config.Logger, Logger: zapLogger,
} }
if dnsOptions := options.DNS01Challenge; dnsOptions != nil && dnsOptions.Provider != "" { if dnsOptions := options.DNS01Challenge; dnsOptions != nil && dnsOptions.Provider != "" {
var solver certmagic.DNS01Solver 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) { GetConfigForCert: func(certificate certmagic.Certificate) (*certmagic.Config, error) {
return config, nil return config, nil
}, },
Logger: zapLogger,
}) })
config = certmagic.New(cache, *config) config = certmagic.New(cache, *config)
var tlsConfig *tls.Config var tlsConfig *tls.Config

View File

@@ -9,8 +9,9 @@ import (
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions" 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`) 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 var err error
if options.ACME != nil && len(options.ACME.Domain) > 0 { if options.ACME != nil && len(options.ACME.Domain) > 0 {
//nolint:staticcheck //nolint:staticcheck
tlsConfig, acmeService, err = startACME(ctx, common.PtrValueOrDefault(options.ACME)) tlsConfig, acmeService, err = startACME(ctx, logger, common.PtrValueOrDefault(options.ACME))
if err != nil { if err != nil {
return nil, err 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 return
} }
resp.Body.Close() 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) t = uint16(time.Since(start) / time.Millisecond)
return return
} }

View File

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

View File

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

View File

@@ -2,6 +2,27 @@
icon: material/alert-decagram 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
_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.11 ### 1.11.11
* Fixes and improvements * Fixes and improvements

View File

@@ -63,7 +63,7 @@ icon: material/new-box
{ {
"external_controller": "0.0.0.0:9090", "external_controller": "0.0.0.0:9090",
"external_ui": "dashboard" "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_controller": "0.0.0.0:9090",
"external_ui": "dashboard" "external_ui": "dashboard"
// external_ui_download_detour: "direct" // "external_ui_download_detour": "direct"
} }
``` ```

View File

@@ -399,10 +399,10 @@ UDP NAT 过期时间。
TCP/IP 栈。 TCP/IP 栈。
| 栈 | 描述 | | 栈 | 描述 |
|--------|------------------------------------------------------------------| |----------|-------------------------------------------------------------------------------------------------------|
| system | 基于系统网络栈执行 L3 到 L4 转换 | | `system` | 基于系统网络栈执行 L3 到 L4 转换 |
| gVisor | 基于 [gVisor](https://github.com/google/gvisor) 虚拟网络栈执行 L3 到 L4 转换 | | `gvisor` | 基于 [gVisor](https://github.com/google/gvisor) 虚拟网络栈执行 L3 到 L4 转换 |
| mixed | 混合 `system` TCP 栈与 `gvisor` UDP 栈 | | `mixed` | 混合 `system` TCP 栈与 `gvisor` UDP 栈 |
默认使用 `mixed` 栈如果 gVisor 构建标记已启用,否则默认使用 `system` 栈。 默认使用 `mixed` 栈如果 gVisor 构建标记已启用,否则默认使用 `system` 栈。

View File

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

View File

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

@@ -14,35 +14,22 @@
{ {
"type": "wireguard", "type": "wireguard",
"tag": "wireguard-out", "tag": "wireguard-out",
"system":false,
"name":"",
"mtu": 1408, "mtu": 1408,
"address":[], "address": null,
"private_key": "", "private_key": "",
"listen_port": 10000, "listen_port": 10000,
"peers": [ "peers": [
{ {
"address": "example.com", "address": "example.com",
"port": 10001, "port": 10001,
"public_key":"", "reserved": "AAAA"
"pre_shared_key":"",
"allowed_ips":[],
"persistent_keepalive_interval":0,
"reserved":[
0,
0,
0
]
} }
], ],
"udp_timeout":"", "udp_timeout": "5m0s",
"workers":0,
"amnezia": { "amnezia": {
"jc": 120, "jc": 120,
"jmin": 23, "jmin": 23,
"jmax": 911, "jmax": 911,
"s1":0,
"s2":0,
"h1": 1, "h1": 1,
"h2": 2, "h2": 2,
"h3": 3, "h3": 3,

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

@@ -14,19 +14,8 @@
{ {
"type": "warp", "type": "warp",
"tag": "warp-out", "tag": "warp-out",
"system":false,
"name":"",
"listen_port": 10000, "listen_port": 10000,
"udp_timeout":"", "udp_timeout": "5m0s",
"workers":0,
"profile":{
"detour":"direct",
"recreate":false,
// for getting existing WARP device profile
"id":"",
"private_key":"",
"auth_token":""
},
"amnezia": { "amnezia": {
"jc": 120, "jc": 120,
"jmin": 23, "jmin": 23,
@@ -35,6 +24,13 @@
"h2": 2, "h2": 2,
"h3": 3, "h3": 3,
"h4": 4 "h4": 4
},
"profile": {
"detour": "direct",
// for getting existing WARP device profile
"id": "",
"private_key": "",
"auth_token": ""
} }
// Dial Fields // Dial Fields
} }

View File

@@ -31,11 +31,8 @@
"tls": { "tls": {
"enabled": true, "enabled": true,
"server_name": "example.com", "server_name": "example.com",
"alpn":[ "alpn": "h2" // h3 for QUIC
"h2" // h3 for QUIC
]
}, },
"packet_encoding":"",
"transport": { "transport": {
"type": "xhttp", "type": "xhttp",
"mode": "stream-up", "mode": "stream-up",
@@ -51,19 +48,14 @@
"h_keep_alive_period": 60 "h_keep_alive_period": 60
}, },
"download": { "download": {
"server":"example.com", "mode": "",
"server_port":443,
"host": "example.com", "host": "example.com",
"path": "/xhttp", "path": "/xhttp",
"domain_strategy": "prefer_ipv4", "domain_strategy": "prefer_ipv4",
"detour":"direct", "x_padding_bytes": "0-0",
"tls":{ "sc_max_each_post_bytes": "0-0",
"enabled":true, "sc_min_posts_interval_ms": "0-0",
"server_name":"example.com", "sc_stream_up_server_secs": "0-0",
"alpn":[
"h2" // h3 for QUIC
]
},
"xmux": { "xmux": {
"max_concurrency": "0-1", "max_concurrency": "0-1",
"max_connections": "0-1", "max_connections": "0-1",
@@ -71,9 +63,18 @@
"h_max_request_times": "0-1", "h_max_request_times": "0-1",
"h_max_reusable_secs": "0-1", "h_max_reusable_secs": "0-1",
"h_keep_alive_period": 60 "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", "type": "dns",

View File

@@ -16,25 +16,23 @@
"tag": "vless-out", "tag": "vless-out",
"listen": "0.0.0.0", "listen": "0.0.0.0",
"listen_port": 443, "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": [ "users": [
{ {
"name": "user", "name": "user",
"uuid": "3179dce2-2ff9-413c-85b4-c1d53ed41668" "uuid": "3179dce2-2ff9-413c-85b4-c1d53ed41668"
} }
], ],
"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": { "transport": {
"type": "xhttp", "type": "xhttp",
"mode": "stream-up", "mode": "stream-up",
"path":"/xhttp" "path": "/xhttp",
} }
} }
], ],

View File

@@ -69,6 +69,10 @@ func NewService(configContent string, platformInterface PlatformInterface) (*Box
cancel() cancel()
return nil, E.Cause(err, "create service") 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() runtimeDebug.FreeOSMemory()
return &BoxService{ return &BoxService{
ctx: ctx, ctx: ctx,

30
go.mod
View File

@@ -1,6 +1,6 @@
module github.com/sagernet/sing-box module github.com/sagernet/sing-box
go 1.24 go 1.24.1
toolchain go1.24.3 toolchain go1.24.3
@@ -8,6 +8,7 @@ require (
github.com/caddyserver/certmagic v0.20.0 github.com/caddyserver/certmagic v0.20.0
github.com/cloudflare/circl v1.6.1 github.com/cloudflare/circl v1.6.1
github.com/cretz/bine v0.2.0 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/chi/v5 v5.2.1
github.com/go-chi/render v1.0.3 github.com/go-chi/render v1.0.3
github.com/gofrs/uuid/v5 v5.3.2 github.com/gofrs/uuid/v5 v5.3.2
@@ -29,15 +30,15 @@ require (
github.com/sagernet/gvisor v0.0.0-20241123041152-536d05261cff github.com/sagernet/gvisor v0.0.0-20241123041152-536d05261cff
github.com/sagernet/quic-go v0.49.0-beta.1 github.com/sagernet/quic-go v0.49.0-beta.1
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691
github.com/sagernet/sing v0.6.9 github.com/sagernet/sing v0.6.10
github.com/sagernet/sing-dns v0.4.3 github.com/sagernet/sing-dns v0.4.6
github.com/sagernet/sing-mux v0.3.2 github.com/sagernet/sing-mux v0.3.2
github.com/sagernet/sing-quic v0.4.1-0.20250423030647-0eb05f373a76 github.com/sagernet/sing-quic v0.4.4
github.com/sagernet/sing-shadowsocks v0.2.7 github.com/sagernet/sing-shadowsocks v0.2.8
github.com/sagernet/sing-shadowsocks2 v0.2.0 github.com/sagernet/sing-shadowsocks2 v0.2.1
github.com/sagernet/sing-shadowtls v0.2.0 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.1 github.com/sagernet/sing-vmess v0.2.3
github.com/sagernet/smux v1.5.34-mod.2 github.com/sagernet/smux v1.5.34-mod.2
github.com/sagernet/utls v1.6.7 github.com/sagernet/utls v1.6.7
github.com/sagernet/wireguard-go v0.0.1-beta.7 github.com/sagernet/wireguard-go v0.0.1-beta.7
@@ -47,11 +48,11 @@ require (
go.uber.org/zap v1.27.0 go.uber.org/zap v1.27.0
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
golang.org/x/crypto v0.38.0 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/mod v0.24.0
golang.org/x/net v0.40.0 golang.org/x/net v0.40.0
golang.org/x/sys v0.33.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 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
google.golang.org/grpc v1.72.1 google.golang.org/grpc v1.72.1
google.golang.org/protobuf v1.36.6 google.golang.org/protobuf v1.36.6
@@ -59,6 +60,9 @@ require (
) )
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/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a // indirect github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a // indirect
github.com/onsi/ginkgo/v2 v2.9.7 // indirect github.com/onsi/ginkgo/v2 v2.9.7 // indirect
@@ -108,10 +112,14 @@ require (
github.com/zeebo/blake3 v0.2.3 // indirect github.com/zeebo/blake3 v0.2.3 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // 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/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/blake3 v1.4.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/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

63
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 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= 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 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/caddyserver/certmagic v0.20.0 h1:bTw7LcEZAh9ucYCRXyCpIrSAGplplI0vGYJ4BpCQ/Fc= 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/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 h1:CaO/zOnF8VvUfEbhRatPcwKVWamvbYd8tQGRWacE9kU=
github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1/go.mod h1:+hnT3ywWDTAFrW5aE+u2Sa/wT555ZqwoCS+pk3p6ry4= 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 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 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= github.com/getlantern/wireguard-go v0.0.1-beta.5.0.20250303165430-793006c422ec h1:jXekDkSozctYj5JQlV1mCsIW+qHpIkgSFhOIpgRSB1I=
@@ -130,30 +136,33 @@ github.com/sagernet/quic-go v0.49.0-beta.1 h1:3LdoCzVVfYRibZns1tYWSIoB65fpTmrwy+
github.com/sagernet/quic-go v0.49.0-beta.1/go.mod h1:uesWD1Ihrldq1M3XtjuEvIUqi8WHNsRs71b3Lt1+p/U= github.com/sagernet/quic-go v0.49.0-beta.1/go.mod h1:uesWD1Ihrldq1M3XtjuEvIUqi8WHNsRs71b3Lt1+p/U=
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byLGkEnIYp6grlXfo1QYUfiYFGjewIdc= github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byLGkEnIYp6grlXfo1QYUfiYFGjewIdc=
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU= github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU=
github.com/sagernet/sing v0.6.9 h1:y/XJH17oyBd6hxgQtKnIdLXu7TsOHxO5i1JeVfVmjXw=
github.com/sagernet/sing v0.6.9/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/sagernet/sing v0.6.9/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing-dns v0.4.3 h1:R6X9oWYbdZ0Mm+8PkdqrBkqx3JwiAbnETUZGOpEdY4E= github.com/sagernet/sing v0.6.10 h1:Jey1tePgH9bjFuK1fQI3D9T+bPOQ4SdHMjuS4sYjDv4=
github.com/sagernet/sing-dns v0.4.3/go.mod h1:dweQs54ng2YGzoJfz+F9dGuDNdP5pJ3PLeggnK5VWc8= github.com/sagernet/sing v0.6.10/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing-mux v0.3.2 h1:meZVFiiStvHThb/trcpAkCrmtJOuItG5Dzl1RRP5/NE= 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-mux v0.3.2/go.mod h1:pht8iFY4c9Xltj7rhVd208npkNaeCxzyXCgulDPLUDA=
github.com/sagernet/sing-quic v0.4.1-0.20250423030647-0eb05f373a76 h1:iwpCX6H3nZEOGUGwx0q5azcgYOA9f6v9YssihXoRKHk= github.com/sagernet/sing-quic v0.4.4 h1:qqOCLnzHbqKkj/wBcXEI3rhSyqoGlqDdv2S6mz2d/JA=
github.com/sagernet/sing-quic v0.4.1-0.20250423030647-0eb05f373a76/go.mod h1:tqPa0/Wqa19MkkSlKVZZX5sHxtiDR9BROcn4ufcbVdY= github.com/sagernet/sing-quic v0.4.4/go.mod h1:tqPa0/Wqa19MkkSlKVZZX5sHxtiDR9BROcn4ufcbVdY=
github.com/sagernet/sing-shadowsocks v0.2.7 h1:zaopR1tbHEw5Nk6FAkM05wCslV6ahVegEZaKMv9ipx8= github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE=
github.com/sagernet/sing-shadowsocks v0.2.7/go.mod h1:0rIKJZBR65Qi0zwdKezt4s57y/Tl1ofkaq6NlkzVuyE= github.com/sagernet/sing-shadowsocks v0.2.8/go.mod h1:lo7TWEMDcN5/h5B8S0ew+r78ZODn6SwVaFhvB6H+PTI=
github.com/sagernet/sing-shadowsocks2 v0.2.0 h1:wpZNs6wKnR7mh1wV9OHwOyUr21VkS3wKFHi+8XwgADg= github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnqqs2gQ2/Qioo=
github.com/sagernet/sing-shadowsocks2 v0.2.0/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ= 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 h1:cLKe4OAOFwuhmAIuPLj//CIL7Q9js+pIDardhJ+/osk=
github.com/sagernet/sing-shadowtls v0.2.0/go.mod h1:agU+Fw5X+xnWVyRHyFthoZCX3MfWKCFPm4JUf+1oaxo= 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.9 h1:uP8O4Q7U9QesjWumgxd2S9fjT3c6aEPWl5RB6uBdVB8=
github.com/sagernet/sing-tun v0.6.5/go.mod h1:fisFCbC4Vfb6HqQNcwPJi2CDK2bf0Xapyz3j3t4cnHE= github.com/sagernet/sing-tun v0.6.9/go.mod h1:fisFCbC4Vfb6HqQNcwPJi2CDK2bf0Xapyz3j3t4cnHE=
github.com/sagernet/sing-vmess v0.2.1 h1:6izHC2+B68aQCxTagki6eZZc+g5eh4dYwxOV5a2Lhug= github.com/sagernet/sing-vmess v0.2.3 h1:z6Ym8dnZG7k1fP3+54vz8G0tvRVJeOoTFFeUPwXTD44=
github.com/sagernet/sing-vmess v0.2.1/go.mod h1:jDAZ0A0St1zVRkyvhAPRySOFfhC+4SQtO5VYyeFotgA= github.com/sagernet/sing-vmess v0.2.3/go.mod h1:jDAZ0A0St1zVRkyvhAPRySOFfhC+4SQtO5VYyeFotgA=
github.com/sagernet/smux v1.5.34-mod.2 h1:gkmBjIjlJ2zQKpLigOkFur5kBKdV6bNRoFu2WkltRQ4= github.com/sagernet/smux v1.5.34-mod.2 h1:gkmBjIjlJ2zQKpLigOkFur5kBKdV6bNRoFu2WkltRQ4=
github.com/sagernet/smux v1.5.34-mod.2/go.mod h1:0KW0+R+ycvA2INW4gbsd7BNyg+HEfLIAxa5N02/28Zc= github.com/sagernet/smux v1.5.34-mod.2/go.mod h1:0KW0+R+ycvA2INW4gbsd7BNyg+HEfLIAxa5N02/28Zc=
github.com/sagernet/utls v1.6.7 h1:Ep3+aJ8FUGGta+II2IEVNUc3EDhaRCZINWkj/LloIA8= 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/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 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc=
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854/go.mod h1:LtfoSK3+NG57tvnVEHgcuBW9ujgE8enPSgzgwStwCAA= 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 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= 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= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
@@ -190,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= 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 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 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.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= 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 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= 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.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= 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 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
@@ -213,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.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 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= 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-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= 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 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= 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= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
@@ -239,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.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= 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/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 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.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 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
@@ -250,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/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 h1:CawjfCvYQH2OU3/TnxLx97WDSUDRABfT18pCOYwc2GE=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6/go.mod h1:3rxYc4HtVcSG9gVaTs2GEBdehh+sYPOwKtyUWEOTb80= 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-20250311190419-81fb87f6b8bf h1:dHDlF3CWxQkefK9IJx+O8ldY0gLygvrlYRBNbPqDWuY=
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/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA= google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA=
google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 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/dns"
"github.com/sagernet/sing-box/protocol/group" "github.com/sagernet/sing-box/protocol/group"
"github.com/sagernet/sing-box/protocol/http" "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/mixed"
"github.com/sagernet/sing-box/protocol/naive" "github.com/sagernet/sing-box/protocol/naive"
"github.com/sagernet/sing-box/protocol/redirect" "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/tor"
"github.com/sagernet/sing-box/protocol/trojan" "github.com/sagernet/sing-box/protocol/trojan"
"github.com/sagernet/sing-box/protocol/tun" "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/vless"
"github.com/sagernet/sing-box/protocol/vmess" "github.com/sagernet/sing-box/protocol/vmess"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
@@ -75,6 +77,7 @@ func OutboundRegistry() *outbound.Registry {
ssh.RegisterOutbound(registry) ssh.RegisterOutbound(registry)
shadowtls.RegisterOutbound(registry) shadowtls.RegisterOutbound(registry)
vless.RegisterOutbound(registry) vless.RegisterOutbound(registry)
mieru.RegisterOutbound(registry)
registerQUICOutbounds(registry) registerQUICOutbounds(registry)
registerWireGuardOutbound(registry) registerWireGuardOutbound(registry)
@@ -86,6 +89,9 @@ func OutboundRegistry() *outbound.Registry {
func EndpointRegistry() *endpoint.Registry { func EndpointRegistry() *endpoint.Registry {
registry := endpoint.NewRegistry() registry := endpoint.NewRegistry()
tunnel.RegisterServerEndpoint(registry)
tunnel.RegisterClientEndpoint(registry)
registerWireGuardEndpoint(registry) registerWireGuardEndpoint(registry)
return registry return registry

View File

@@ -6,6 +6,7 @@ type ExperimentalOptions struct {
CacheFile *CacheFileOptions `json:"cache_file,omitempty"` CacheFile *CacheFileOptions `json:"cache_file,omitempty"`
ClashAPI *ClashAPIOptions `json:"clash_api,omitempty"` ClashAPI *ClashAPIOptions `json:"clash_api,omitempty"`
V2RayAPI *V2RayAPIOptions `json:"v2ray_api,omitempty"` V2RayAPI *V2RayAPIOptions `json:"v2ray_api,omitempty"`
UnifiedDelay *UnifiedDelayOptions `json:"unified_delay,omitempty"`
Debug *DebugOptions `json:"debug,omitempty"` Debug *DebugOptions `json:"debug,omitempty"`
} }
@@ -53,3 +54,7 @@ type V2RayStatsServiceOptions struct {
Outbounds []string `json:"outbounds,omitempty"` Outbounds []string `json:"outbounds,omitempty"`
Users []string `json:"users,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"` SourcePortRange badoption.Listable[string] `json:"source_port_range,omitempty"`
Port badoption.Listable[uint16] `json:"port,omitempty"` Port badoption.Listable[uint16] `json:"port,omitempty"`
PortRange badoption.Listable[string] `json:"port_range,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"` ProcessName badoption.Listable[string] `json:"process_name,omitempty"`
ProcessPath badoption.Listable[string] `json:"process_path,omitempty"` ProcessPath badoption.Listable[string] `json:"process_path,omitempty"`
ProcessPathRegex badoption.Listable[string] `json:"process_path_regex,omitempty"` ProcessPathRegex badoption.Listable[string] `json:"process_path_regex,omitempty"`

View File

@@ -144,6 +144,7 @@ type RouteActionOptions struct {
type RawRouteOptionsActionOptions struct { type RawRouteOptionsActionOptions struct {
OverrideAddress string `json:"override_address,omitempty"` OverrideAddress string `json:"override_address,omitempty"`
OverridePort uint16 `json:"override_port,omitempty"` OverridePort uint16 `json:"override_port,omitempty"`
OverrideTunnelDestination string `json:"override_tunnel_destination,omitempty"`
NetworkStrategy *NetworkStrategy `json:"network_strategy,omitempty"` NetworkStrategy *NetworkStrategy `json:"network_strategy,omitempty"`
FallbackDelay uint32 `json:"fallback_delay,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"` SourcePortRange badoption.Listable[string] `json:"source_port_range,omitempty"`
Port badoption.Listable[uint16] `json:"port,omitempty"` Port badoption.Listable[uint16] `json:"port,omitempty"`
PortRange badoption.Listable[string] `json:"port_range,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"` ProcessName badoption.Listable[string] `json:"process_name,omitempty"`
ProcessPath badoption.Listable[string] `json:"process_path,omitempty"` ProcessPath badoption.Listable[string] `json:"process_path,omitempty"`
ProcessPathRegex badoption.Listable[string] `json:"process_path_regex,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"` SourcePortRange badoption.Listable[string] `json:"source_port_range,omitempty"`
Port badoption.Listable[uint16] `json:"port,omitempty"` Port badoption.Listable[uint16] `json:"port,omitempty"`
PortRange badoption.Listable[string] `json:"port_range,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"` ProcessName badoption.Listable[string] `json:"process_name,omitempty"`
ProcessPath badoption.Listable[string] `json:"process_path,omitempty"` ProcessPath badoption.Listable[string] `json:"process_path,omitempty"`
ProcessPathRegex badoption.Listable[string] `json:"process_path_regex,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) { func (s *Selector) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
ctx = interrupt.ContextWithIsExternalConnection(ctx) ctx = interrupt.ContextWithIsExternalConnection(ctx)
selected := s.selected.Load() selected := s.selected.Load()
conn = s.interruptGroup.NewConn(conn, interrupt.IsExternalConnectionFromContext(ctx))
if outboundHandler, isHandler := selected.(adapter.ConnectionHandlerEx); isHandler { if outboundHandler, isHandler := selected.(adapter.ConnectionHandlerEx); isHandler {
outboundHandler.NewConnectionEx(ctx, conn, metadata, onClose) outboundHandler.NewConnectionEx(ctx, conn, metadata, onClose)
} else { } 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) { func (s *Selector) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
ctx = interrupt.ContextWithIsExternalConnection(ctx) ctx = interrupt.ContextWithIsExternalConnection(ctx)
selected := s.selected.Load() selected := s.selected.Load()
conn = s.interruptGroup.NewSingPacketConn(conn, interrupt.IsExternalConnectionFromContext(ctx))
if outboundHandler, isHandler := selected.(adapter.PacketConnectionHandlerEx); isHandler { if outboundHandler, isHandler := selected.(adapter.PacketConnectionHandlerEx); isHandler {
outboundHandler.NewPacketConnectionEx(ctx, conn, metadata, onClose) outboundHandler.NewPacketConnectionEx(ctx, conn, metadata, onClose)
} else { } 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) { func (s *URLTest) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
ctx = interrupt.ContextWithIsExternalConnection(ctx) ctx = interrupt.ContextWithIsExternalConnection(ctx)
conn = s.group.interruptGroup.NewConn(conn, interrupt.IsExternalConnectionFromContext(ctx))
s.connection.NewConnection(ctx, s, conn, metadata, onClose) s.connection.NewConnection(ctx, s, conn, metadata, onClose)
} }
func (s *URLTest) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { func (s *URLTest) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
ctx = interrupt.ContextWithIsExternalConnection(ctx) ctx = interrupt.ContextWithIsExternalConnection(ctx)
conn = s.group.interruptGroup.NewSingPacketConn(conn, interrupt.IsExternalConnectionFromContext(ctx))
s.connection.NewPacketConnection(ctx, s, conn, metadata, onClose) 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
}

View File

@@ -214,7 +214,6 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
if !loaded { if !loaded {
return nil, E.New("parse route_address_set: rule-set not found: ", routeAddressSet) return nil, E.New("parse route_address_set: rule-set not found: ", routeAddressSet)
} }
ruleSet.IncRef()
inbound.routeRuleSet = append(inbound.routeRuleSet, ruleSet) inbound.routeRuleSet = append(inbound.routeRuleSet, ruleSet)
} }
for _, routeExcludeAddressSet := range options.RouteExcludeAddressSet { for _, routeExcludeAddressSet := range options.RouteExcludeAddressSet {
@@ -222,7 +221,6 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
if !loaded { if !loaded {
return nil, E.New("parse route_exclude_address_set: rule-set not found: ", routeExcludeAddressSet) return nil, E.New("parse route_exclude_address_set: rule-set not found: ", routeExcludeAddressSet)
} }
ruleSet.IncRef()
inbound.routeExcludeRuleSet = append(inbound.routeExcludeRuleSet, ruleSet) inbound.routeExcludeRuleSet = append(inbound.routeExcludeRuleSet, ruleSet)
} }
if options.AutoRedirect { if options.AutoRedirect {
@@ -312,7 +310,7 @@ func (t *Inbound) Start(stage adapter.StartStage) error {
if len(ipSets) == 0 { if len(ipSets) == 0 {
t.logger.Warn("route_address_set: no destination IP CIDR rules found in rule-set: ", routeRuleSet.Name()) t.logger.Warn("route_address_set: no destination IP CIDR rules found in rule-set: ", routeRuleSet.Name())
} }
routeRuleSet.DecRef() routeRuleSet.IncRef()
t.routeAddressSet = append(t.routeAddressSet, ipSets...) t.routeAddressSet = append(t.routeAddressSet, ipSets...)
if t.autoRedirect != nil { if t.autoRedirect != nil {
t.routeRuleSetCallback = append(t.routeRuleSetCallback, routeRuleSet.RegisterCallback(t.updateRouteAddressSet)) t.routeRuleSetCallback = append(t.routeRuleSetCallback, routeRuleSet.RegisterCallback(t.updateRouteAddressSet))
@@ -324,7 +322,7 @@ func (t *Inbound) Start(stage adapter.StartStage) error {
if len(ipSets) == 0 { if len(ipSets) == 0 {
t.logger.Warn("route_address_set: no destination IP CIDR rules found in rule-set: ", routeExcludeRuleSet.Name()) t.logger.Warn("route_address_set: no destination IP CIDR rules found in rule-set: ", routeExcludeRuleSet.Name())
} }
routeExcludeRuleSet.DecRef() routeExcludeRuleSet.IncRef()
t.routeExcludeAddressSet = append(t.routeExcludeAddressSet, ipSets...) t.routeExcludeAddressSet = append(t.routeExcludeAddressSet, ipSets...)
if t.autoRedirect != nil { if t.autoRedirect != nil {
t.routeExcludeRuleSetCallback = append(t.routeExcludeRuleSetCallback, routeExcludeRuleSet.RegisterCallback(t.updateRouteAddressSet)) t.routeExcludeRuleSetCallback = append(t.routeExcludeRuleSetCallback, routeExcludeRuleSet.RegisterCallback(t.updateRouteAddressSet))

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 var metadata adapter.InboundContext
metadata.Source = source metadata.Source = source
metadata.Destination = destination 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) h.logger.InfoContext(ctx, "inbound connection from ", metadata.Source)
(*Inbound)(h).NewConnectionEx(ctx, conn, metadata, onClose) (*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 var metadata adapter.InboundContext
metadata.Source = source metadata.Source = source
metadata.Destination = destination 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) h.logger.InfoContext(ctx, "inbound connection from ", metadata.Source)
(*Inbound)(h).NewConnectionEx(ctx, conn, metadata, onClose) (*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 { func (w *WARPEndpoint) Close() error {
if err := w.isEndpointInitialized(); err != nil { if ok := w.isEndpointInitialized(); !ok {
return err return E.New("endpoint not initialized")
} }
return w.endpoint.Close() return w.endpoint.Close()
} }
func (w *WARPEndpoint) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { func (w *WARPEndpoint) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
if err := w.isEndpointInitialized(); err != nil { if ok := w.isEndpointInitialized(); !ok {
return nil, err return nil, E.New("endpoint not initialized")
} }
return w.endpoint.DialContext(ctx, network, destination) return w.endpoint.DialContext(ctx, network, destination)
} }
func (w *WARPEndpoint) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { func (w *WARPEndpoint) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
if err := w.isEndpointInitialized(); err != nil { if ok := w.isEndpointInitialized(); !ok {
return nil, err return nil, E.New("endpoint not initialized")
} }
return w.endpoint.ListenPacket(ctx, destination) return w.endpoint.ListenPacket(ctx, destination)
} }
func (w *WARPEndpoint) isEndpointInitialized() error { func (w *WARPEndpoint) isEndpointInitialized() bool {
w.mtx.Lock() w.mtx.Lock()
defer w.mtx.Unlock() defer w.mtx.Unlock()
if w.endpoint == nil { return w.endpoint != nil
return E.New("endpoint not initialized")
}
return nil
} }

View File

@@ -2,15 +2,14 @@ package route
import ( import (
"context" "context"
"errors"
"net" "net"
"time" "time"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
dnsOutbound "github.com/sagernet/sing-box/protocol/dns" 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-dns"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/buf"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata" 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 { if natConn, isNatConn := conn.(udpnat.Conn); isNatConn {
metadata.Destination = M.Socksaddr{} metadata.Destination = M.Socksaddr{}
for _, packet := range packetBuffers { for _, packet := range packetBuffers {
@@ -45,10 +44,12 @@ func (r *Router) hijackDNSPacket(ctx context.Context, conn N.PacketConn, packetB
conn: conn, conn: conn,
ctx: ctx, ctx: ctx,
metadata: metadata, metadata: metadata,
onClose: onClose,
}) })
return return
} }
err := dnsOutbound.NewDNSPacketConnection(ctx, r, conn, packetBuffers, metadata) err := dnsOutbound.NewDNSPacketConnection(ctx, r, conn, packetBuffers, metadata)
N.CloseOnHandshakeFailure(conn, onClose, err)
if err != nil && !E.IsClosedOrCanceled(err) { if err != nil && !E.IsClosedOrCanceled(err) {
r.dnsLogger.ErrorContext(ctx, E.Cause(err, "process packet connection")) 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) { 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) 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")) router.dnsLogger.ErrorContext(ctx, E.Cause(err, "process packet connection"))
} }
} }
@@ -85,8 +86,16 @@ type dnsHijacker struct {
conn N.PacketConn conn N.PacketConn
ctx context.Context ctx context.Context
metadata adapter.InboundContext metadata adapter.InboundContext
onClose N.CloseHandlerFunc
} }
func (h *dnsHijacker) NewPacketEx(buffer *buf.Buffer, destination M.Socksaddr) { func (h *dnsHijacker) NewPacketEx(buffer *buf.Buffer, destination M.Socksaddr) {
go ExchangeDNSPacket(h.ctx, h.router, h.conn, buffer, h.metadata, destination) 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" "github.com/sagernet/sing-box/common/sniff"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option" "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-dns"
"github.com/sagernet/sing-mux" "github.com/sagernet/sing-mux"
"github.com/sagernet/sing-vmess" "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) err := r.routeConnection(ctx, conn, metadata, onClose)
if err != nil { if err != nil {
N.CloseOnHandshakeFailure(conn, onClose, err) N.CloseOnHandshakeFailure(conn, onClose, err)
if E.IsClosedOrCanceled(err) { if E.IsClosedOrCanceled(err) || R.IsRejected(err) {
r.logger.DebugContext(ctx, "connection closed: ", err) r.logger.DebugContext(ctx, "connection closed: ", err)
} else { } else {
r.logger.ErrorContext(ctx, err) 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 var selectedOutbound adapter.Outbound
if selectedRule != nil { if selectedRule != nil {
switch action := selectedRule.Action().(type) { switch action := selectedRule.Action().(type) {
case *rule.RuleActionRoute: case *R.RuleActionRoute:
var loaded bool var loaded bool
selectedOutbound, loaded = r.outbound.Outbound(action.Outbound) selectedOutbound, loaded = r.outbound.Outbound(action.Outbound)
if !loaded { if !loaded {
@@ -112,15 +112,15 @@ func (r *Router) routeConnection(ctx context.Context, conn net.Conn, metadata ad
buf.ReleaseMulti(buffers) buf.ReleaseMulti(buffers)
return E.New("TCP is not supported by outbound: ", selectedOutbound.Tag()) return E.New("TCP is not supported by outbound: ", selectedOutbound.Tag())
} }
case *rule.RuleActionReject: case *R.RuleActionReject:
buf.ReleaseMulti(buffers) buf.ReleaseMulti(buffers)
N.CloseOnHandshakeFailure(conn, onClose, action.Error(ctx)) N.CloseOnHandshakeFailure(conn, onClose, action.Error(ctx))
return nil return nil
case *rule.RuleActionHijackDNS: case *R.RuleActionHijackDNS:
for _, buffer := range buffers { for _, buffer := range buffers {
conn = bufio.NewCachedConn(conn, buffer) conn = bufio.NewCachedConn(conn, buffer)
} }
r.hijackDNSStream(ctx, conn, metadata) N.CloseOnHandshakeFailure(conn, onClose, r.hijackDNSStream(ctx, conn, metadata))
return nil return nil
} }
} }
@@ -154,7 +154,7 @@ func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, m
})) }))
if err != nil { if err != nil {
conn.Close() conn.Close()
if E.IsClosedOrCanceled(err) { if E.IsClosedOrCanceled(err) || R.IsRejected(err) {
r.logger.DebugContext(ctx, "connection closed: ", err) r.logger.DebugContext(ctx, "connection closed: ", err)
} else { } else {
r.logger.ErrorContext(ctx, err) r.logger.ErrorContext(ctx, err)
@@ -171,13 +171,11 @@ func (r *Router) RoutePacketConnectionEx(ctx context.Context, conn N.PacketConn,
err := r.routePacketConnection(ctx, conn, metadata, onClose) err := r.routePacketConnection(ctx, conn, metadata, onClose)
if err != nil { if err != nil {
N.CloseOnHandshakeFailure(conn, onClose, err) N.CloseOnHandshakeFailure(conn, onClose, err)
if E.IsClosedOrCanceled(err) { if E.IsClosedOrCanceled(err) || R.IsRejected(err) {
r.logger.DebugContext(ctx, "connection closed: ", err) r.logger.DebugContext(ctx, "connection closed: ", err)
} else { } else {
r.logger.ErrorContext(ctx, err) r.logger.ErrorContext(ctx, err)
} }
} else if onClose != nil {
onClose(nil)
} }
} }
@@ -219,7 +217,7 @@ func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, m
var selectReturn bool var selectReturn bool
if selectedRule != nil { if selectedRule != nil {
switch action := selectedRule.Action().(type) { switch action := selectedRule.Action().(type) {
case *rule.RuleActionRoute: case *R.RuleActionRoute:
var loaded bool var loaded bool
selectedOutbound, loaded = r.outbound.Outbound(action.Outbound) selectedOutbound, loaded = r.outbound.Outbound(action.Outbound)
if !loaded { if !loaded {
@@ -230,12 +228,12 @@ func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, m
N.ReleaseMultiPacketBuffer(packetBuffers) N.ReleaseMultiPacketBuffer(packetBuffers)
return E.New("UDP is not supported by outbound: ", selectedOutbound.Tag()) return E.New("UDP is not supported by outbound: ", selectedOutbound.Tag())
} }
case *rule.RuleActionReject: case *R.RuleActionReject:
N.ReleaseMultiPacketBuffer(packetBuffers) N.ReleaseMultiPacketBuffer(packetBuffers)
N.CloseOnHandshakeFailure(conn, onClose, action.Error(ctx)) N.CloseOnHandshakeFailure(conn, onClose, action.Error(ctx))
return nil return nil
case *rule.RuleActionHijackDNS: case *R.RuleActionHijackDNS:
r.hijackDNSPacket(ctx, conn, packetBuffers, metadata) r.hijackDNSPacket(ctx, conn, packetBuffers, metadata, onClose)
return nil return nil
} }
} }
@@ -273,7 +271,7 @@ func (r *Router) PreMatch(metadata adapter.InboundContext) error {
if selectedRule == nil { if selectedRule == nil {
return nil return nil
} }
rejectAction, isReject := selectedRule.Action().(*rule.RuleActionReject) rejectAction, isReject := selectedRule.Action().(*R.RuleActionReject)
if !isReject { if !isReject {
return nil return nil
} }
@@ -348,7 +346,7 @@ func (r *Router) matchRule(
//nolint:staticcheck //nolint:staticcheck
if metadata.InboundOptions != common.DefaultValue[option.InboundOptions]() { if metadata.InboundOptions != common.DefaultValue[option.InboundOptions]() {
if !preMatch && metadata.InboundOptions.SniffEnabled { 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, OverrideDestination: metadata.InboundOptions.SniffOverrideDestination,
Timeout: time.Duration(metadata.InboundOptions.SniffTimeout), Timeout: time.Duration(metadata.InboundOptions.SniffTimeout),
}, inputConn, inputPacketConn, nil) }, inputConn, inputPacketConn, nil)
@@ -363,7 +361,7 @@ func (r *Router) matchRule(
} }
} }
if dns.DomainStrategy(metadata.InboundOptions.DomainStrategy) != dns.DomainStrategyAsIS { 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), Strategy: dns.DomainStrategy(metadata.InboundOptions.DomainStrategy),
}) })
if fatalErr != nil { if fatalErr != nil {
@@ -400,11 +398,11 @@ match:
} }
} }
} }
var routeOptions *rule.RuleActionRouteOptions var routeOptions *R.RuleActionRouteOptions
switch action := currentRule.Action().(type) { switch action := currentRule.Action().(type) {
case *rule.RuleActionRoute: case *R.RuleActionRoute:
routeOptions = &action.RuleActionRouteOptions routeOptions = &action.RuleActionRouteOptions
case *rule.RuleActionRouteOptions: case *R.RuleActionRouteOptions:
routeOptions = action routeOptions = action
} }
if routeOptions != nil { if routeOptions != nil {
@@ -427,6 +425,9 @@ match:
Fqdn: metadata.Destination.Fqdn, Fqdn: metadata.Destination.Fqdn,
} }
} }
if routeOptions.OverrideTunnelDestination != "" {
metadata.TunnelDestination = routeOptions.OverrideTunnelDestination
}
if routeOptions.NetworkStrategy != nil { if routeOptions.NetworkStrategy != nil {
metadata.NetworkStrategy = routeOptions.NetworkStrategy metadata.NetworkStrategy = routeOptions.NetworkStrategy
} }
@@ -450,7 +451,7 @@ match:
} }
} }
switch action := currentRule.Action().(type) { switch action := currentRule.Action().(type) {
case *rule.RuleActionSniff: case *R.RuleActionSniff:
if !preMatch { if !preMatch {
newBuffer, newPacketBuffers, newErr := r.actionSniff(ctx, metadata, action, inputConn, inputPacketConn, buffers) newBuffer, newPacketBuffers, newErr := r.actionSniff(ctx, metadata, action, inputConn, inputPacketConn, buffers)
if newErr != nil { if newErr != nil {
@@ -467,7 +468,7 @@ match:
selectedRuleIndex = currentRuleIndex selectedRuleIndex = currentRuleIndex
break match break match
} }
case *rule.RuleActionResolve: case *R.RuleActionResolve:
fatalErr = r.actionResolve(ctx, metadata, action) fatalErr = r.actionResolve(ctx, metadata, action)
if fatalErr != nil { if fatalErr != nil {
return return
@@ -487,7 +488,7 @@ match:
} }
func (r *Router) actionSniff( 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, inputConn net.Conn, inputPacketConn N.PacketConn, inputBuffers []*buf.Buffer,
) (buffer *buf.Buffer, packetBuffers []*N.PacketBuffer, fatalErr error) { ) (buffer *buf.Buffer, packetBuffers []*N.PacketBuffer, fatalErr error) {
if sniff.Skip(metadata) { if sniff.Skip(metadata) {
@@ -498,7 +499,12 @@ func (r *Router) actionSniff(
return return
} }
if inputConn != nil { if inputConn != nil {
sniffBuffer := buf.NewPacket() 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 var streamSniffers []sniff.StreamSniffer
if len(action.StreamSniffers) > 0 { if len(action.StreamSniffers) > 0 {
streamSniffers = action.StreamSniffers streamSniffers = action.StreamSniffers
@@ -512,6 +518,7 @@ func (r *Router) actionSniff(
sniff.RDP, sniff.RDP,
} }
} }
sniffBuffer := buf.NewPacket()
err := sniff.PeekStream( err := sniff.PeekStream(
ctx, ctx,
metadata, metadata,
@@ -521,6 +528,7 @@ func (r *Router) actionSniff(
action.Timeout, action.Timeout,
streamSniffers..., streamSniffers...,
) )
metadata.SniffError = err
if err == nil { if err == nil {
//goland:noinspection GoDeprecation //goland:noinspection GoDeprecation
if action.OverrideDestination && M.IsDomainName(metadata.Domain) { if action.OverrideDestination && M.IsDomainName(metadata.Domain) {
@@ -543,9 +551,24 @@ func (r *Router) actionSniff(
sniffBuffer.Release() sniffBuffer.Release()
} }
} else if inputPacketConn != nil { } else if inputPacketConn != nil {
if metadata.PacketSniffError != nil && !errors.Is(metadata.PacketSniffError, sniff.ErrNeedMoreData) { if len(action.PacketSniffers) == 0 && len(action.StreamSniffers) > 0 {
r.logger.DebugContext(ctx, "packet sniff skipped due to previous error: ", metadata.PacketSniffError)
return 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 packetSniffers []sniff.PacketSniffer
if len(action.PacketSniffers) > 0 {
packetSniffers = action.PacketSniffers
} else {
packetSniffers = []sniff.PacketSniffer{
sniff.DomainNameQuery,
sniff.QUICClientHello,
sniff.STUNMessage,
sniff.UTP,
sniff.UDPTracker,
sniff.DTLSRecord,
}
} }
for { for {
var ( var (
@@ -578,7 +601,7 @@ func (r *Router) actionSniff(
return return
} }
} else { } else {
if len(packetBuffers) > 0 || metadata.PacketSniffError != nil { if len(packetBuffers) > 0 || metadata.SniffError != nil {
err = sniff.PeekPacket( err = sniff.PeekPacket(
ctx, ctx,
metadata, metadata,
@@ -586,19 +609,6 @@ func (r *Router) actionSniff(
sniff.QUICClientHello, sniff.QUICClientHello,
) )
} else { } else {
var packetSniffers []sniff.PacketSniffer
if len(action.PacketSniffers) > 0 {
packetSniffers = action.PacketSniffers
} else {
packetSniffers = []sniff.PacketSniffer{
sniff.DomainNameQuery,
sniff.QUICClientHello,
sniff.STUNMessage,
sniff.UTP,
sniff.UDPTracker,
sniff.DTLSRecord,
}
}
err = sniff.PeekPacket( err = sniff.PeekPacket(
ctx, metadata, ctx, metadata,
sniffBuffer.Bytes(), sniffBuffer.Bytes(),
@@ -611,7 +621,7 @@ func (r *Router) actionSniff(
Destination: destination, Destination: destination,
} }
packetBuffers = append(packetBuffers, packetBuffer) packetBuffers = append(packetBuffers, packetBuffer)
metadata.PacketSniffError = err metadata.SniffError = err
if errors.Is(err, sniff.ErrNeedMoreData) { if errors.Is(err, sniff.ErrNeedMoreData) {
// TODO: replace with generic message when there are more multi-packet protocols // TODO: replace with generic message when there are more multi-packet protocols
r.logger.DebugContext(ctx, "attempt to sniff fragmented QUIC client hello") r.logger.DebugContext(ctx, "attempt to sniff fragmented QUIC client hello")
@@ -642,7 +652,7 @@ func (r *Router) actionSniff(
return 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() { if metadata.Destination.IsFqdn() {
metadata.DNSServer = action.Server metadata.DNSServer = action.Server
addresses, err := r.Lookup(adapter.WithContext(ctx, metadata), metadata.Destination.Fqdn, action.Strategy) 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]}, Question: []mDNS.Question{message.Question[0]},
}, nil }, nil
case C.RuleActionRejectMethodDrop: 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: case C.RuleActionRejectMethodDefault:
return nil, nil return nil, nil
case C.RuleActionRejectMethodDrop: case C.RuleActionRejectMethodDrop:
return nil, tun.ErrDrop return nil, &R.RejectedError{Cause: tun.ErrDrop}
} }
} }
} }

View File

@@ -2,6 +2,7 @@ package rule
import ( import (
"context" "context"
"errors"
"net/netip" "net/netip"
"strings" "strings"
"sync" "sync"
@@ -33,6 +34,7 @@ func NewRuleAction(ctx context.Context, logger logger.ContextLogger, action opti
RuleActionRouteOptions: RuleActionRouteOptions{ RuleActionRouteOptions: RuleActionRouteOptions{
OverrideAddress: M.ParseSocksaddrHostPort(action.RouteOptions.OverrideAddress, 0), OverrideAddress: M.ParseSocksaddrHostPort(action.RouteOptions.OverrideAddress, 0),
OverridePort: action.RouteOptions.OverridePort, OverridePort: action.RouteOptions.OverridePort,
OverrideTunnelDestination: action.RouteOptions.OverrideTunnelDestination,
NetworkStrategy: (*C.NetworkStrategy)(action.RouteOptions.NetworkStrategy), NetworkStrategy: (*C.NetworkStrategy)(action.RouteOptions.NetworkStrategy),
FallbackDelay: time.Duration(action.RouteOptions.FallbackDelay), FallbackDelay: time.Duration(action.RouteOptions.FallbackDelay),
UDPDisableDomainUnmapping: action.RouteOptions.UDPDisableDomainUnmapping, UDPDisableDomainUnmapping: action.RouteOptions.UDPDisableDomainUnmapping,
@@ -147,6 +149,7 @@ func (r *RuleActionRoute) String() string {
type RuleActionRouteOptions struct { type RuleActionRouteOptions struct {
OverrideAddress M.Socksaddr OverrideAddress M.Socksaddr
OverridePort uint16 OverridePort uint16
OverrideTunnelDestination string
NetworkStrategy *C.NetworkStrategy NetworkStrategy *C.NetworkStrategy
NetworkType []C.InterfaceType NetworkType []C.InterfaceType
FallbackNetworkType []C.InterfaceType FallbackNetworkType []C.InterfaceType
@@ -168,6 +171,9 @@ func (r *RuleActionRouteOptions) String() string {
if r.OverridePort > 0 { if r.OverridePort > 0 {
descriptions = append(descriptions, F.ToString("override-port=", r.OverridePort)) 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 { if r.NetworkStrategy != nil {
descriptions = append(descriptions, F.ToString("network-strategy=", r.NetworkStrategy)) descriptions = append(descriptions, F.ToString("network-strategy=", r.NetworkStrategy))
} }
@@ -250,6 +256,23 @@ func (r *RuleActionDirect) String() string {
return "direct" + r.description 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 { type RuleActionReject struct {
Method string Method string
NoDrop bool NoDrop bool
@@ -273,9 +296,9 @@ func (r *RuleActionReject) Error(ctx context.Context) error {
var returnErr error var returnErr error
switch r.Method { switch r.Method {
case C.RuleActionRejectMethodDefault: case C.RuleActionRejectMethodDefault:
returnErr = syscall.ECONNREFUSED returnErr = &RejectedError{syscall.ECONNREFUSED}
case C.RuleActionRejectMethodDrop: case C.RuleActionRejectMethodDrop:
return tun.ErrDrop return &RejectedError{tun.ErrDrop}
default: default:
panic(F.ToString("unknown reject method: ", r.Method)) panic(F.ToString("unknown reject method: ", r.Method))
} }
@@ -293,7 +316,7 @@ func (r *RuleActionReject) Error(ctx context.Context) error {
if ctx != nil { if ctx != nil {
r.logger.DebugContext(ctx, "dropped due to flooding") r.logger.DebugContext(ctx, "dropped due to flooding")
} }
return tun.ErrDrop return &RejectedError{tun.ErrDrop}
} }
return returnErr 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.destinationPortItems = append(rule.destinationPortItems, item)
rule.allItems = append(rule.allItems, 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 { if len(options.ProcessName) > 0 {
item := NewProcessItem(options.ProcessName) item := NewProcessItem(options.ProcessName)
rule.items = append(rule.items, item) 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.destinationPortItems = append(rule.destinationPortItems, item)
rule.allItems = append(rule.allItems, 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 { if len(options.ProcessName) > 0 {
item := NewProcessItem(options.ProcessName) item := NewProcessItem(options.ProcessName)
rule.items = append(rule.items, item) 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.destinationPortItems = append(rule.destinationPortItems, item)
rule.allItems = append(rule.allItems, 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 { if len(options.ProcessName) > 0 {
item := NewProcessItem(options.ProcessName) item := NewProcessItem(options.ProcessName)
rule.items = append(rule.items, item) 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 { func NewHTTP1Conn(conn net.Conn, request *http.Request) *HTTPConn {
if request.Header.Get("Host") == "" {
request.Header.Set("Host", request.Host)
}
return &HTTPConn{ return &HTTPConn{
Conn: conn, Conn: conn,
request: request, request: request,
@@ -89,9 +92,6 @@ func (c *HTTPConn) writeRequest(payload []byte) error {
if err != nil { if err != nil {
return err return err
} }
if c.request.Header.Get("Host") == "" {
c.request.Header.Set("Host", c.request.Host)
}
for key, value := range c.request.Header { for key, value := range c.request.Header {
_, err = writer.Write([]byte(F.ToString(key, ": ", strings.Join(value, ", "), CRLF))) _, err = writer.Write([]byte(F.ToString(key, ": ", strings.Join(value, ", "), CRLF)))
if err != nil { if err != nil {

View File

@@ -47,6 +47,7 @@ func NewServer(ctx context.Context, logger logger.ContextLogger, options option.
server := &Server{ server := &Server{
ctx: ctx, ctx: ctx,
tlsConfig: tlsConfig, tlsConfig: tlsConfig,
logger: logger,
handler: handler, handler: handler,
h2Server: &http2.Server{ h2Server: &http2.Server{
IdleTimeout: time.Duration(options.IdleTimeout), IdleTimeout: time.Duration(options.IdleTimeout),

View File

@@ -3,6 +3,7 @@ package v2raywebsocket
import ( import (
"context" "context"
"encoding/base64" "encoding/base64"
"errors"
"io" "io"
"net" "net"
"os" "os"
@@ -67,9 +68,10 @@ func (c *WebsocketConn) Read(b []byte) (n int, err error) {
return return
} }
if !E.IsMulti(err, io.EOF, wsutil.ErrNoFrameAdvance) { if !E.IsMulti(err, io.EOF, wsutil.ErrNoFrameAdvance) {
err = wrapWsError(err)
return return
} }
header, err = c.reader.NextFrame() header, err = wrapWsError0(c.reader.NextFrame())
if err != nil { if err != nil {
return return
} }
@@ -78,14 +80,14 @@ func (c *WebsocketConn) Read(b []byte) (n int, err error) {
err = wsutil.ErrFrameTooLarge err = wsutil.ErrFrameTooLarge
return return
} }
err = c.controlHandler(header, c.reader) err = wrapWsError(c.controlHandler(header, c.reader))
if err != nil { if err != nil {
return return
} }
continue continue
} }
if header.OpCode&ws.OpBinary == 0 { if header.OpCode&ws.OpBinary == 0 {
err = c.reader.Discard() err = wrapWsError(c.reader.Discard())
if err != nil { if err != nil {
return return
} }
@@ -95,7 +97,7 @@ func (c *WebsocketConn) Read(b []byte) (n int, err error) {
} }
func (c *WebsocketConn) Write(p []byte) (n int, err error) { func (c *WebsocketConn) Write(p []byte) (n int, err error) {
err = wsutil.WriteMessage(c.Conn, c.state, ws.OpBinary, p) err = wrapWsError(wsutil.WriteMessage(c.Conn, c.state, ws.OpBinary, p))
if err != nil { if err != nil {
return return
} }
@@ -146,7 +148,7 @@ func (c *EarlyWebsocketConn) Read(b []byte) (n int, err error) {
return 0, c.err return 0, c.err
} }
} }
return c.conn.Read(b) return wrapWsError0(c.conn.Read(b))
} }
func (c *EarlyWebsocketConn) writeRequest(content []byte) error { func (c *EarlyWebsocketConn) writeRequest(content []byte) error {
@@ -191,7 +193,7 @@ func (c *EarlyWebsocketConn) writeRequest(content []byte) error {
func (c *EarlyWebsocketConn) Write(b []byte) (n int, err error) { func (c *EarlyWebsocketConn) Write(b []byte) (n int, err error) {
if c.conn != nil { if c.conn != nil {
return c.conn.Write(b) return wrapWsError0(c.conn.Write(b))
} }
c.access.Lock() c.access.Lock()
defer c.access.Unlock() defer c.access.Unlock()
@@ -199,7 +201,7 @@ func (c *EarlyWebsocketConn) Write(b []byte) (n int, err error) {
return 0, c.err return 0, c.err
} }
if c.conn != nil { if c.conn != nil {
return c.conn.Write(b) return wrapWsError0(c.conn.Write(b))
} }
err = c.writeRequest(b) err = c.writeRequest(b)
c.err = err c.err = err
@@ -212,12 +214,12 @@ func (c *EarlyWebsocketConn) Write(b []byte) (n int, err error) {
func (c *EarlyWebsocketConn) WriteBuffer(buffer *buf.Buffer) error { func (c *EarlyWebsocketConn) WriteBuffer(buffer *buf.Buffer) error {
if c.conn != nil { if c.conn != nil {
return c.conn.WriteBuffer(buffer) return wrapWsError(c.conn.WriteBuffer(buffer))
} }
c.access.Lock() c.access.Lock()
defer c.access.Unlock() defer c.access.Unlock()
if c.conn != nil { if c.conn != nil {
return c.conn.WriteBuffer(buffer) return wrapWsError(c.conn.WriteBuffer(buffer))
} }
if c.err != nil { if c.err != nil {
return c.err return c.err
@@ -272,3 +274,23 @@ func (c *EarlyWebsocketConn) Upstream() any {
func (c *EarlyWebsocketConn) LazyHeadroom() bool { func (c *EarlyWebsocketConn) LazyHeadroom() bool {
return c.conn == nil return c.conn == nil
} }
func wrapWsError(err error) error {
if err == nil {
return nil
}
var closedErr wsutil.ClosedError
if errors.As(err, &closedErr) {
if closedErr.Code == ws.StatusNormalClosure {
err = io.EOF
}
}
return err
}
func wrapWsError0[T any](value T, err error) (T, error) {
if err == nil {
return value, nil
}
return value, wrapWsError(err)
}

View File

@@ -1,22 +0,0 @@
package v2raywebsocket
import (
"net"
"time"
)
type deadConn struct {
net.Conn
}
func (c *deadConn) SetDeadline(t time.Time) error {
return nil
}
func (c *deadConn) SetReadDeadline(t time.Time) error {
return nil
}
func (c *deadConn) SetWriteDeadline(t time.Time) error {
return nil
}

View File

@@ -66,7 +66,7 @@ func (w *Writer) WriteBuffer(buffer *buf.Buffer) error {
ws.Cipher(data, *(*[4]byte)(header[1+payloadBitLength:]), 0) ws.Cipher(data, *(*[4]byte)(header[1+payloadBitLength:]), 0)
} }
return w.writer.WriteBuffer(buffer) return wrapWsError(w.writer.WriteBuffer(buffer))
} }
func (w *Writer) FrontHeadroom() int { func (w *Writer) FrontHeadroom() int {

View File

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