Add Snell protocol. Refactor MASQUE HTTP/2, Fair Queue. Update XHTTP, OpenVPN, Sudoku, Fallback. Fixes

This commit is contained in:
Shtorm
2026-06-26 01:25:57 +03:00
parent d174962a04
commit edf38d33d6
107 changed files with 5346 additions and 708 deletions

View File

@@ -80,6 +80,7 @@ func (h *Inbound) Start(stage adapter.StartStage) error {
}
func (h *Inbound) Close() error {
h.conns.Close()
errs := make([]error, 0)
for _, inbound := range h.inbounds {
err := inbound.Close()

View File

@@ -4,6 +4,7 @@ import (
"context"
"net"
"sync"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/adapter/outbound"
@@ -31,14 +32,19 @@ type Fallback struct {
tags []string
outbounds map[string]adapter.Outbound
lastUsedOutbound string
mtx sync.Mutex
blacklistTimeout time.Duration
blacklist map[string]time.Time
mtx sync.Mutex
}
func NewFallback(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.FallbackOutboundOptions) (adapter.Outbound, error) {
if len(options.Outbounds) == 0 {
return nil, E.New("missing tags")
}
blacklistTimeout := time.Duration(options.BlacklistTimeout)
if blacklistTimeout == 0 {
blacklistTimeout = time.Minute
}
outbound := &Fallback{
Adapter: outbound.NewAdapter(C.TypeFallback, tag, []string{N.NetworkTCP, N.NetworkUDP}, options.Outbounds),
ctx: ctx,
@@ -47,6 +53,8 @@ func NewFallback(ctx context.Context, router adapter.Router, logger log.ContextL
tags: options.Outbounds,
outbounds: make(map[string]adapter.Outbound, len(options.Outbounds)),
lastUsedOutbound: options.Outbounds[0],
blacklistTimeout: blacklistTimeout,
blacklist: make(map[string]time.Time),
}
return outbound, nil
}
@@ -73,35 +81,110 @@ func (s *Fallback) All() []string {
}
func (s *Fallback) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
var conn net.Conn
s.mtx.Lock()
var active, blacklisted []string
for _, tag := range s.tags {
if s.isBlacklisted(tag) {
blacklisted = append(blacklisted, tag)
} else {
active = append(active, tag)
}
}
s.mtx.Unlock()
var err error
for _, outbound := range s.outbounds {
conn, err = outbound.DialContext(ctx, network, destination)
for _, tag := range active {
var conn net.Conn
conn, err = s.outbounds[tag].DialContext(ctx, network, destination)
if err != nil {
s.logger.InfoContext(ctx, err)
s.mtx.Lock()
s.addToBlacklist(tag)
s.mtx.Unlock()
continue
}
s.mtx.Lock()
s.lastUsedOutbound = tag
s.mtx.Unlock()
return conn, nil
}
for _, tag := range blacklisted {
var conn net.Conn
conn, err = s.outbounds[tag].DialContext(ctx, network, destination)
if err != nil {
s.logger.InfoContext(ctx, err)
continue
}
s.mtx.Lock()
defer s.mtx.Unlock()
s.lastUsedOutbound = outbound.Tag()
delete(s.blacklist, tag)
s.lastUsedOutbound = tag
s.mtx.Unlock()
return conn, nil
}
return nil, err
}
func (s *Fallback) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
var conn net.PacketConn
s.mtx.Lock()
var active, blacklisted []string
for _, tag := range s.tags {
if s.isBlacklisted(tag) {
blacklisted = append(blacklisted, tag)
} else {
active = append(active, tag)
}
}
s.mtx.Unlock()
var err error
for _, outbound := range s.outbounds {
conn, err = outbound.ListenPacket(ctx, destination)
for _, tag := range active {
var conn net.PacketConn
conn, err = s.outbounds[tag].ListenPacket(ctx, destination)
if err != nil {
s.logger.InfoContext(ctx, err)
s.mtx.Lock()
s.addToBlacklist(tag)
s.mtx.Unlock()
continue
}
s.mtx.Lock()
s.lastUsedOutbound = tag
s.mtx.Unlock()
return conn, nil
}
for _, tag := range blacklisted {
var conn net.PacketConn
conn, err = s.outbounds[tag].ListenPacket(ctx, destination)
if err != nil {
s.logger.InfoContext(ctx, err)
continue
}
s.mtx.Lock()
defer s.mtx.Unlock()
s.lastUsedOutbound = outbound.Tag()
delete(s.blacklist, tag)
s.lastUsedOutbound = tag
s.mtx.Unlock()
return conn, nil
}
return nil, err
}
func (s *Fallback) isBlacklisted(tag string) bool {
if s.blacklistTimeout == 0 {
return false
}
expiry, ok := s.blacklist[tag]
if !ok {
return false
}
if time.Now().After(expiry) {
delete(s.blacklist, tag)
return false
}
return true
}
func (s *Fallback) addToBlacklist(tag string) {
if s.blacklistTimeout > 0 {
s.blacklist[tag] = time.Now().Add(s.blacklistTimeout)
}
}

View File

@@ -2,11 +2,11 @@ package bandwidth
import (
"context"
"slices"
"sync"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/list"
)
type BandwidthLimiter interface {
@@ -14,123 +14,144 @@ type BandwidthLimiter interface {
SetSpeed(speed uint64)
}
type FlowKeysLimiter struct {
type FairQueueLimiter struct {
limiter BandwidthLimiter
connIDGetter ConnIDGetter
waits map[string][]*wait
conns map[string]int
flows *list.List[*flow]
index map[string]*list.Element[*flow]
bytes map[string]uint64
pool sync.Pool
queue chan struct{}
reset time.Time
mtx sync.Mutex
}
func NewFlowKeysLimiter(connIDGetter ConnIDGetter, limiter BandwidthLimiter) *FlowKeysLimiter {
return &FlowKeysLimiter{
func NewFairQueueLimiter(connIDGetter ConnIDGetter, limiter BandwidthLimiter) *FairQueueLimiter {
return &FairQueueLimiter{
limiter: limiter,
connIDGetter: connIDGetter,
waits: make(map[string][]*wait),
conns: make(map[string]int),
flows: list.New[*flow](),
index: make(map[string]*list.Element[*flow]),
bytes: make(map[string]uint64),
pool: sync.Pool{New: func() any { return list.New[*request]() }},
queue: make(chan struct{}, 1),
reset: time.Now().Add(time.Second),
}
}
func (l *FlowKeysLimiter) SetSpeed(speed uint64) {
func (l *FairQueueLimiter) SetSpeed(speed uint64) {
l.limiter.SetSpeed(speed)
}
func (l *FlowKeysLimiter) WaitN(ctx context.Context, n int) error {
func (l *FairQueueLimiter) WaitN(ctx context.Context, n int) error {
id, _ := l.connIDGetter(ctx, adapter.ContextFrom(ctx))
mainWait := &wait{ctx, make(chan struct{}), n}
mainRequest := &request{ctx: ctx, done: make(chan struct{}), n: n}
l.mtx.Lock()
if waits, ok := l.waits[id]; ok {
l.waits[id] = append(waits, mainWait)
} else {
l.waits[id] = []*wait{mainWait}
elem, ok := l.index[id]
if !ok {
f := &flow{id: id, pending: l.pool.Get().(*list.List[*request])}
elem = l.flows.PushFront(f)
l.index[id] = elem
}
mainRequestElem := elem.Value.pending.PushBack(mainRequest)
l.reorder(elem)
l.mtx.Unlock()
select {
case l.queue <- struct{}{}:
case <-mainWait.finish:
case <-mainRequest.done:
return nil
case <-ctx.Done():
l.mtx.Lock()
for i, wait := range l.waits[id] {
if wait == mainWait {
l.waits[id] = slices.Delete(l.waits[id], i, i+1)
close(wait.finish)
break
}
}
l.removeRequest(id, mainRequestElem)
l.mtx.Unlock()
return ctx.Err()
}
select {
case <-mainRequest.done:
<-l.queue
return nil
default:
}
for {
if ctx.Err() != nil {
l.mtx.Lock()
for i, wait := range l.waits[id] {
if wait == mainWait {
l.waits[id] = slices.Delete(l.waits[id], i, i+1)
close(wait.finish)
break
}
}
l.removeRequest(id, mainRequestElem)
l.mtx.Unlock()
<-l.queue
return ctx.Err()
}
l.mtx.Lock()
now := time.Now()
if l.reset.Compare(now) == -1 {
clear(l.conns)
clear(l.bytes)
l.reset = now.Add(time.Second)
}
l.mtx.Lock()
var minConnId string
var minN int
for connID, waits := range l.waits {
if len(waits) == 0 {
continue
}
if n, ok := l.conns[connID]; ok {
if minConnId == "" {
minConnId = connID
minN = n
continue
}
if n+waits[0].n < minN {
minConnId = connID
minN = n
}
} else {
l.conns[connID] = 0
minConnId = connID
break
}
}
minWait := l.waits[minConnId][0]
l.waits[minConnId][0] = nil
l.waits[minConnId] = l.waits[minConnId][1:]
if len(l.waits) == 0 {
delete(l.waits, minConnId)
flowElem := l.flows.Front()
flow := flowElem.Value
firstRequestElem := flow.pending.Front()
firstRequest := firstRequestElem.Value
l.bytes[flow.id] += uint64(firstRequest.n)
firstRequestElem.Remove()
if flow.pending.Len() == 0 {
l.flows.Remove(flowElem)
delete(l.index, flow.id)
l.pool.Put(flow.pending)
} else {
l.reorder(flowElem)
}
l.mtx.Unlock()
err := l.limiter.WaitN(ctx, minWait.n)
if err != nil {
continue
}
l.conns[minConnId] = l.conns[minConnId] + minWait.n
close(minWait.finish)
if minWait == mainWait {
l.limiter.WaitN(firstRequest.ctx, firstRequest.n)
close(firstRequest.done)
if firstRequest == mainRequest {
<-l.queue
return nil
}
}
}
type wait struct {
ctx context.Context
finish chan struct{}
n int
func (l *FairQueueLimiter) reorder(elem *list.Element[*flow]) {
f := elem.Value
front := f.pending.Front()
if front == nil {
return
}
cost := l.bytes[f.id] + uint64(front.Value.n)
for e := l.flows.Front(); e != nil; e = e.Next() {
if e == elem {
continue
}
eFront := e.Value.pending.Front()
if eFront == nil {
continue
}
if cost < l.bytes[e.Value.id]+uint64(eFront.Value.n) {
l.flows.MoveBefore(elem, e)
return
}
}
l.flows.MoveToBack(elem)
}
func (l *FairQueueLimiter) removeRequest(id string, elem *list.Element[*request]) {
if !elem.Remove() {
return
}
if flowElem, ok := l.index[id]; ok && flowElem.Value.pending.Len() == 0 {
l.flows.Remove(flowElem)
delete(l.index, id)
l.pool.Put(flowElem.Value.pending)
}
}
type flow struct {
id string
pending *list.List[*request]
}
type request struct {
ctx context.Context
done chan struct{}
n int
}

View File

@@ -357,7 +357,7 @@ func createSpeedLimiter(speed uint64, flowKeys []string) (BandwidthLimiter, erro
if err != nil {
return nil, err
}
limiter = NewFlowKeysLimiter(getter, limiter)
limiter = NewFairQueueLimiter(getter, limiter)
}
return limiter, nil
}

View File

@@ -11,6 +11,7 @@ import (
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/adapter/outbound"
"github.com/sagernet/sing-box/common/cloudflare"
"github.com/sagernet/sing-box/common/congestion"
"github.com/sagernet/sing-box/common/dialer"
"github.com/sagernet/sing-box/common/tls"
C "github.com/sagernet/sing-box/constant"
@@ -23,6 +24,7 @@ import (
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/ntp"
"github.com/sagernet/sing/service"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
)
@@ -132,6 +134,15 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
logger.ErrorContext(ctx, err)
return
}
congestionControl, err := congestion.NewCongestionControl(
options.CongestionController,
options.CWND,
ntp.TimeFuncFromContext(ctx),
)
if err != nil {
logger.ErrorContext(ctx, err)
return
}
tunnel, err := masque.NewTunnel(
ctx,
logger,
@@ -156,6 +167,7 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
UDPKeepalivePeriod: udpKeepalivePeriod,
UDPInitialPacketSize: options.UDPInitialPacketSize,
ReconnectDelay: options.ReconnectDelay.Build(),
CongestionControl: congestionControl,
},
)
if err != nil {

View File

@@ -104,6 +104,7 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
AllowedAddress: options.AllowedIPs,
ReconnectDelay: time.Duration(options.ReconnectDelay),
PingInterval: time.Duration(options.PingInterval),
PingRestart: time.Duration(options.PingRestart),
})
if err != nil {
return nil, err

130
protocol/snell/inbound.go Normal file
View File

@@ -0,0 +1,130 @@
package snell
import (
"context"
"net"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/adapter/inbound"
"github.com/sagernet/sing-box/common/listener"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/transport/snell"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/bufio"
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"
)
func RegisterInbound(registry *inbound.Registry) {
inbound.Register[option.SnellInboundOptions](registry, C.TypeSnell, NewInbound)
}
type Inbound struct {
inbound.Adapter
router adapter.ConnectionRouterEx
logger logger.ContextLogger
listener *listener.Listener
service *snell.Service
}
func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SnellInboundOptions) (adapter.Inbound, error) {
if options.PSK == "" {
return nil, E.New("snell requires psk")
}
udpEnabled := common.Contains(options.Network.Build(), N.NetworkUDP)
obfsMode := ""
if options.Obfs != nil {
obfsMode = options.Obfs.Mode
}
in := &Inbound{
Adapter: inbound.NewAdapter(C.TypeSnell, tag),
router: router,
logger: logger,
}
service, err := snell.NewService(snell.ServiceOptions{
PSK: []byte(options.PSK),
Version: options.Version,
ObfsMode: obfsMode,
UDP: udpEnabled,
Logger: logger,
Handler: (*inboundHandler)(in),
})
if err != nil {
return nil, err
}
in.service = service
in.listener = listener.New(listener.Options{
Context: ctx,
Logger: logger,
Network: []string{N.NetworkTCP},
Listen: options.ListenOptions,
ConnectionHandler: in,
})
return in, nil
}
func (h *Inbound) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateStart {
return nil
}
return h.listener.Start()
}
func (h *Inbound) Close() error {
return h.listener.Close()
}
func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
err := h.service.NewConnection(ctx, conn, metadata.Source)
N.CloseOnHandshakeFailure(conn, onClose, err)
if err != nil {
h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source))
}
}
var _ adapter.TCPInjectableInbound = (*Inbound)(nil)
type inboundHandler Inbound
func (h *inboundHandler) NewConnection(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, clientID string) {
var metadata adapter.InboundContext
metadata.Inbound = h.Tag()
metadata.InboundType = h.Type()
metadata.InboundDetour = h.listener.ListenOptions().Detour
metadata.Source = source
metadata.Destination = destination
if clientID != "" {
metadata.User = clientID
h.logger.InfoContext(ctx, "[", clientID, "] inbound connection to ", destination)
} else {
h.logger.InfoContext(ctx, "inbound connection to ", destination)
}
done := make(chan struct{})
h.router.RouteConnectionEx(ctx, conn, metadata, N.OnceClose(func(error) {
close(done)
}))
<-done
}
func (h *inboundHandler) NewPacketConnection(ctx context.Context, conn net.PacketConn, source M.Socksaddr, clientID string) {
var metadata adapter.InboundContext
metadata.Inbound = h.Tag()
metadata.InboundType = h.Type()
metadata.InboundDetour = h.listener.ListenOptions().Detour
metadata.Source = source
if clientID != "" {
metadata.User = clientID
h.logger.InfoContext(ctx, "[", clientID, "] inbound packet connection")
} else {
h.logger.InfoContext(ctx, "inbound packet connection")
}
done := make(chan struct{})
h.router.RoutePacketConnectionEx(ctx, bufio.NewPacketConn(conn), metadata, N.OnceClose(func(error) {
close(done)
}))
<-done
}

114
protocol/snell/outbound.go Normal file
View File

@@ -0,0 +1,114 @@
package snell
import (
"context"
"fmt"
"net"
"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-box/transport/snell"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/bufio"
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"
)
func RegisterOutbound(registry *outbound.Registry) {
outbound.Register[option.SnellOutboundOptions](registry, C.TypeSnell, NewOutbound)
}
type Outbound struct {
outbound.Adapter
logger logger.ContextLogger
client *snell.Client
}
func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SnellOutboundOptions) (adapter.Outbound, error) {
if options.PSK == "" {
return nil, E.New("snell requires psk")
}
version := options.Version
if version == 0 {
version = snell.DefaultSnellVersion
}
if version == snell.Version5 {
version = snell.Version4
}
udpEnabled := common.Contains(options.Network.Build(), N.NetworkUDP)
switch version {
case snell.Version1, snell.Version2:
if udpEnabled {
return nil, fmt.Errorf("snell version %d does not support UDP", version)
}
case snell.Version3, snell.Version4:
default:
return nil, fmt.Errorf("snell version error: %d", version)
}
reuse := version == snell.Version2 || (version == snell.Version4 && options.Reuse)
obfsMode := ""
obfsHost := "bing.com"
if options.Obfs != nil {
switch options.Obfs.Mode {
case "", "tls", "http":
obfsMode = options.Obfs.Mode
default:
return nil, fmt.Errorf("snell obfs mode error: %s", options.Obfs.Mode)
}
if options.Obfs.Host != "" {
obfsHost = options.Obfs.Host
}
}
outboundDialer, err := dialer.New(ctx, options.DialerOptions, options.ServerIsDomain())
if err != nil {
return nil, err
}
client := snell.NewClient(snell.ClientOptions{
Dialer: outboundDialer,
Server: options.ServerOptions.Build(),
PSK: []byte(options.PSK),
Version: version,
Reuse: reuse,
ObfsMode: obfsMode,
ObfsHost: obfsHost,
})
return &Outbound{
Adapter: outbound.NewAdapterWithDialerOptions(C.TypeSnell, tag, options.Network.Build(), options.DialerOptions),
logger: logger,
client: client,
}, nil
}
func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
ctx, metadata := adapter.ExtendContext(ctx)
metadata.Outbound = h.Tag()
metadata.Destination = destination
switch N.NetworkName(network) {
case N.NetworkTCP:
h.logger.InfoContext(ctx, "outbound connection to ", destination)
return h.client.DialContext(ctx, destination)
case N.NetworkUDP:
h.logger.InfoContext(ctx, "outbound packet connection to ", destination)
conn, err := h.client.ListenPacket(ctx, destination)
if err != nil {
return nil, err
}
return bufio.NewBindPacketConn(conn, destination), nil
default:
return nil, E.Extend(N.ErrUnknownNetwork, network)
}
}
func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
ctx, metadata := adapter.ExtendContext(ctx)
metadata.Outbound = h.Tag()
metadata.Destination = destination
h.logger.InfoContext(ctx, "outbound packet connection to ", destination)
return h.client.ListenPacket(ctx, destination)
}

View File

@@ -10,6 +10,7 @@ import (
"github.com/sagernet/quic-go/http3"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/adapter/inbound"
"github.com/sagernet/sing-box/common/congestion"
"github.com/sagernet/sing-box/common/listener"
"github.com/sagernet/sing-box/common/tls"
C "github.com/sagernet/sing-box/constant"
@@ -136,10 +137,9 @@ func (h *Inbound) Start(stage adapter.StartStage) error {
if err != nil {
return err
}
congestionControlFactory, err := trusttunnel.NewCongestionControl(
congestionControlFactory, err := congestion.NewCongestionControl(
h.options.CongestionController,
h.options.CWND,
h.options.BBRProfile,
ntp.TimeFuncFromContext(h.ctx),
)
if err != nil {

View File

@@ -53,7 +53,7 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
QUIC: options.QUIC,
CongestionControl: options.CongestionController,
CWND: options.CWND,
BBRProfile: options.BBRProfile,
Logger: logger,
HealthCheck: options.HealthCheck,
}
var client trusttunnel.Dialer