mirror of
https://github.com/SagerNet/sing-box.git
synced 2025-07-24 06:54:08 +08:00
Compare commits
109 Commits
f0d7533fa3
...
0decb158f6
Author | SHA1 | Date | |
---|---|---|---|
![]() |
0decb158f6 | ||
![]() |
61dc853128 | ||
![]() |
36b6c364c8 | ||
![]() |
0a5f09f147 | ||
![]() |
e7ae3ddf31 | ||
![]() |
8b15576289 | ||
![]() |
c86f47bdf8 | ||
![]() |
ce013ceabb | ||
![]() |
d0824ea7f6 | ||
![]() |
7291d7e802 | ||
![]() |
3f38520f5e | ||
![]() |
de7e0364e8 | ||
![]() |
867c267bef | ||
![]() |
2dfe93c99c | ||
![]() |
656301bb9e | ||
![]() |
301549f453 | ||
![]() |
cd7229f39b | ||
![]() |
f0e42f8b8f | ||
![]() |
65a629cbb5 | ||
![]() |
7774f16552 | ||
![]() |
2ded81f1f7 | ||
![]() |
e68036ea62 | ||
![]() |
f5dd883fb5 | ||
![]() |
aa2a4ea2bb | ||
![]() |
aeb340170d | ||
![]() |
009b224de6 | ||
![]() |
f6898a7806 | ||
![]() |
44ada21eb2 | ||
![]() |
d276db1ccc | ||
![]() |
1016ba6cef | ||
![]() |
ecfd010ac9 | ||
![]() |
3c07a9dbf5 | ||
![]() |
115eb9e872 | ||
![]() |
26f0c5dd93 | ||
![]() |
0d9883b2c8 | ||
![]() |
b026e78f79 | ||
![]() |
2e20110f02 | ||
![]() |
9fc424498d | ||
![]() |
886ea0c893 | ||
![]() |
fbd79337e5 | ||
![]() |
de31c316a0 | ||
![]() |
ebaaee7b75 | ||
![]() |
1f35f25ada | ||
![]() |
5a1d726242 | ||
![]() |
2f3ebe6f68 | ||
![]() |
c2e962f9da | ||
![]() |
d0052e3803 | ||
![]() |
f2cb235305 | ||
![]() |
d6bd251e2d | ||
![]() |
1f07157ab5 | ||
![]() |
10afe58231 | ||
![]() |
23a99c2af1 | ||
![]() |
8337505d0d | ||
![]() |
9bd951264f | ||
![]() |
7ef938ed05 | ||
![]() |
45d20963c4 | ||
![]() |
e53428481f | ||
![]() |
9d3c47b612 | ||
![]() |
029783522c | ||
![]() |
b7f55e931e | ||
![]() |
e3405c656e | ||
![]() |
264c67f755 | ||
![]() |
b5f5e94fdb | ||
![]() |
3b53ca6e94 | ||
![]() |
6cb3030457 | ||
![]() |
47a717c6c1 | ||
![]() |
2167079ac6 | ||
![]() |
cf77712dc1 | ||
![]() |
883e9dac43 | ||
![]() |
d1ebc72b9d | ||
![]() |
27efb76ac0 | ||
![]() |
ada4633f49 | ||
![]() |
f4b6f828d6 | ||
![]() |
0ddef18bce | ||
![]() |
2e196ebba6 | ||
![]() |
15e41980f3 | ||
![]() |
6e8eff3296 | ||
![]() |
243a40ae1a | ||
![]() |
0d300cd5f8 | ||
![]() |
4879dd6992 | ||
![]() |
ad2ff84166 | ||
![]() |
753f036748 | ||
![]() |
27fa1a730d | ||
![]() |
afdf750da4 | ||
![]() |
75e0ce993f | ||
![]() |
88b4a8ae8e | ||
![]() |
92c639040c | ||
![]() |
f329e859dd | ||
![]() |
4918580820 | ||
![]() |
9dbd482971 | ||
![]() |
7cc6e1c0ae | ||
![]() |
2a03668d15 | ||
![]() |
095721e0e5 | ||
![]() |
7d0d59c9ee | ||
![]() |
18e9eaa244 | ||
![]() |
3439b75e41 | ||
![]() |
7b095b7719 | ||
![]() |
5dd1a09663 | ||
![]() |
750a9be4a9 | ||
![]() |
d21d724345 | ||
![]() |
74f1c3aa09 | ||
![]() |
aef0d477d2 | ||
![]() |
24cf4e9062 | ||
![]() |
dd89730ee7 | ||
![]() |
912bd00f1c | ||
![]() |
3ab6e6c0b0 | ||
![]() |
81ff3eef20 | ||
![]() |
d511698f3f | ||
![]() |
cb435ea232 |
2
box.go
2
box.go
@ -498,7 +498,7 @@ func (s *Box) Close() error {
|
||||
close(s.done)
|
||||
}
|
||||
err := common.Close(
|
||||
s.inbound, s.outbound, s.endpoint, s.router, s.connection, s.dnsRouter, s.dnsTransport, s.network,
|
||||
s.service, s.endpoint, s.inbound, s.outbound, s.router, s.connection, s.dnsRouter, s.dnsTransport, s.network,
|
||||
)
|
||||
for _, lifecycleService := range s.internalService {
|
||||
err = E.Append(err, lifecycleService.Close(), func(err error) error {
|
||||
|
@ -10,7 +10,6 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/bufio"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
@ -25,7 +24,9 @@ type slowOpenConn struct {
|
||||
destination M.Socksaddr
|
||||
conn net.Conn
|
||||
create chan struct{}
|
||||
done chan struct{}
|
||||
access sync.Mutex
|
||||
closeOnce sync.Once
|
||||
err error
|
||||
}
|
||||
|
||||
@ -44,6 +45,7 @@ func DialSlowContext(dialer *tcpDialer, ctx context.Context, network string, des
|
||||
network: network,
|
||||
destination: destination,
|
||||
create: make(chan struct{}),
|
||||
done: make(chan struct{}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -54,8 +56,8 @@ func (c *slowOpenConn) Read(b []byte) (n int, err error) {
|
||||
if c.err != nil {
|
||||
return 0, c.err
|
||||
}
|
||||
case <-c.ctx.Done():
|
||||
return 0, c.ctx.Err()
|
||||
case <-c.done:
|
||||
return 0, os.ErrClosed
|
||||
}
|
||||
}
|
||||
return c.conn.Read(b)
|
||||
@ -73,6 +75,8 @@ func (c *slowOpenConn) Write(b []byte) (n int, err error) {
|
||||
return 0, c.err
|
||||
}
|
||||
return c.conn.Write(b)
|
||||
case <-c.done:
|
||||
return 0, os.ErrClosed
|
||||
default:
|
||||
}
|
||||
conn, err := c.dialer.DialContext(c.ctx, c.network, c.destination.String(), b)
|
||||
@ -87,7 +91,13 @@ func (c *slowOpenConn) Write(b []byte) (n int, err error) {
|
||||
}
|
||||
|
||||
func (c *slowOpenConn) Close() error {
|
||||
return common.Close(c.conn)
|
||||
c.closeOnce.Do(func() {
|
||||
close(c.done)
|
||||
if c.conn != nil {
|
||||
c.conn.Close()
|
||||
}
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *slowOpenConn) LocalAddr() net.Addr {
|
||||
@ -152,8 +162,8 @@ func (c *slowOpenConn) WriteTo(w io.Writer) (n int64, err error) {
|
||||
if c.err != nil {
|
||||
return 0, c.err
|
||||
}
|
||||
case <-c.ctx.Done():
|
||||
return 0, c.ctx.Err()
|
||||
case <-c.done:
|
||||
return 0, c.err
|
||||
}
|
||||
}
|
||||
return bufio.Copy(w, c.conn)
|
||||
|
@ -25,7 +25,7 @@ import (
|
||||
"golang.org/x/crypto/cryptobyte"
|
||||
)
|
||||
|
||||
func parseECHClientConfig(ctx context.Context, options option.OutboundTLSOptions, tlsConfig *tls.Config) (Config, error) {
|
||||
func parseECHClientConfig(ctx context.Context, clientConfig ECHCapableConfig, options option.OutboundTLSOptions) (Config, error) {
|
||||
var echConfig []byte
|
||||
if len(options.ECH.Config) > 0 {
|
||||
echConfig = []byte(strings.Join(options.ECH.Config, "\n"))
|
||||
@ -45,12 +45,12 @@ func parseECHClientConfig(ctx context.Context, options option.OutboundTLSOptions
|
||||
if block == nil || block.Type != "ECH CONFIGS" || len(rest) > 0 {
|
||||
return nil, E.New("invalid ECH configs pem")
|
||||
}
|
||||
tlsConfig.EncryptedClientHelloConfigList = block.Bytes
|
||||
return &STDClientConfig{tlsConfig}, nil
|
||||
clientConfig.SetECHConfigList(block.Bytes)
|
||||
return clientConfig, nil
|
||||
} else {
|
||||
return &STDECHClientConfig{
|
||||
STDClientConfig: STDClientConfig{tlsConfig},
|
||||
dnsRouter: service.FromContext[adapter.DNSRouter](ctx),
|
||||
return &ECHClientConfig{
|
||||
ECHCapableConfig: clientConfig,
|
||||
dnsRouter: service.FromContext[adapter.DNSRouter](ctx),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
@ -102,15 +102,21 @@ func reloadECHKeys(echKeyPath string, tlsConfig *tls.Config) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type STDECHClientConfig struct {
|
||||
STDClientConfig
|
||||
type ECHCapableConfig interface {
|
||||
Config
|
||||
ECHConfigList() []byte
|
||||
SetECHConfigList([]byte)
|
||||
}
|
||||
|
||||
type ECHClientConfig struct {
|
||||
ECHCapableConfig
|
||||
access sync.Mutex
|
||||
dnsRouter adapter.DNSRouter
|
||||
lastTTL time.Duration
|
||||
lastUpdate time.Time
|
||||
}
|
||||
|
||||
func (s *STDECHClientConfig) ClientHandshake(ctx context.Context, conn net.Conn) (aTLS.Conn, error) {
|
||||
func (s *ECHClientConfig) ClientHandshake(ctx context.Context, conn net.Conn) (aTLS.Conn, error) {
|
||||
tlsConn, err := s.fetchAndHandshake(ctx, conn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -122,17 +128,17 @@ func (s *STDECHClientConfig) ClientHandshake(ctx context.Context, conn net.Conn)
|
||||
return tlsConn, nil
|
||||
}
|
||||
|
||||
func (s *STDECHClientConfig) fetchAndHandshake(ctx context.Context, conn net.Conn) (aTLS.Conn, error) {
|
||||
func (s *ECHClientConfig) fetchAndHandshake(ctx context.Context, conn net.Conn) (aTLS.Conn, error) {
|
||||
s.access.Lock()
|
||||
defer s.access.Unlock()
|
||||
if len(s.config.EncryptedClientHelloConfigList) == 0 || s.lastTTL == 0 || time.Now().Sub(s.lastUpdate) > s.lastTTL {
|
||||
if len(s.ECHConfigList()) == 0 || s.lastTTL == 0 || time.Now().Sub(s.lastUpdate) > s.lastTTL {
|
||||
message := &mDNS.Msg{
|
||||
MsgHdr: mDNS.MsgHdr{
|
||||
RecursionDesired: true,
|
||||
},
|
||||
Question: []mDNS.Question{
|
||||
{
|
||||
Name: mDNS.Fqdn(s.config.ServerName),
|
||||
Name: mDNS.Fqdn(s.ServerName()),
|
||||
Qtype: mDNS.TypeHTTPS,
|
||||
Qclass: mDNS.ClassINET,
|
||||
},
|
||||
@ -157,21 +163,21 @@ func (s *STDECHClientConfig) fetchAndHandshake(ctx context.Context, conn net.Con
|
||||
}
|
||||
s.lastTTL = time.Duration(rr.Header().Ttl) * time.Second
|
||||
s.lastUpdate = time.Now()
|
||||
s.config.EncryptedClientHelloConfigList = echConfigList
|
||||
s.SetECHConfigList(echConfigList)
|
||||
break match
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(s.config.EncryptedClientHelloConfigList) == 0 {
|
||||
if len(s.ECHConfigList()) == 0 {
|
||||
return nil, E.New("no ECH config found in DNS records")
|
||||
}
|
||||
}
|
||||
return s.Client(conn)
|
||||
}
|
||||
|
||||
func (s *STDECHClientConfig) Clone() Config {
|
||||
return &STDECHClientConfig{STDClientConfig: STDClientConfig{s.config.Clone()}, dnsRouter: s.dnsRouter, lastUpdate: s.lastUpdate}
|
||||
func (s *ECHClientConfig) Clone() Config {
|
||||
return &ECHClientConfig{ECHCapableConfig: s.ECHCapableConfig.Clone().(ECHCapableConfig), dnsRouter: s.dnsRouter, lastUpdate: s.lastUpdate}
|
||||
}
|
||||
|
||||
func UnmarshalECHKeys(raw []byte) ([]tls.EncryptedClientHelloKey, error) {
|
||||
|
@ -74,7 +74,7 @@ func NewRealityClient(ctx context.Context, serverAddress string, options option.
|
||||
if decodedLen > 8 {
|
||||
return nil, E.New("invalid short_id")
|
||||
}
|
||||
return &RealityClientConfig{ctx, uClient, publicKey, shortID}, nil
|
||||
return &RealityClientConfig{ctx, uClient.(*UTLSClientConfig), publicKey, shortID}, nil
|
||||
}
|
||||
|
||||
func (e *RealityClientConfig) ServerName() string {
|
||||
|
@ -7,43 +7,60 @@ import (
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/tlsfragment"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/ntp"
|
||||
)
|
||||
|
||||
type STDClientConfig struct {
|
||||
config *tls.Config
|
||||
ctx context.Context
|
||||
config *tls.Config
|
||||
fragment bool
|
||||
fragmentFallbackDelay time.Duration
|
||||
recordFragment bool
|
||||
}
|
||||
|
||||
func (s *STDClientConfig) ServerName() string {
|
||||
return s.config.ServerName
|
||||
func (c *STDClientConfig) ServerName() string {
|
||||
return c.config.ServerName
|
||||
}
|
||||
|
||||
func (s *STDClientConfig) SetServerName(serverName string) {
|
||||
s.config.ServerName = serverName
|
||||
func (c *STDClientConfig) SetServerName(serverName string) {
|
||||
c.config.ServerName = serverName
|
||||
}
|
||||
|
||||
func (s *STDClientConfig) NextProtos() []string {
|
||||
return s.config.NextProtos
|
||||
func (c *STDClientConfig) NextProtos() []string {
|
||||
return c.config.NextProtos
|
||||
}
|
||||
|
||||
func (s *STDClientConfig) SetNextProtos(nextProto []string) {
|
||||
s.config.NextProtos = nextProto
|
||||
func (c *STDClientConfig) SetNextProtos(nextProto []string) {
|
||||
c.config.NextProtos = nextProto
|
||||
}
|
||||
|
||||
func (s *STDClientConfig) Config() (*STDConfig, error) {
|
||||
return s.config, nil
|
||||
func (c *STDClientConfig) Config() (*STDConfig, error) {
|
||||
return c.config, nil
|
||||
}
|
||||
|
||||
func (s *STDClientConfig) Client(conn net.Conn) (Conn, error) {
|
||||
return tls.Client(conn, s.config), nil
|
||||
func (c *STDClientConfig) Client(conn net.Conn) (Conn, error) {
|
||||
if c.recordFragment {
|
||||
conn = tf.NewConn(conn, c.ctx, c.fragment, c.recordFragment, c.fragmentFallbackDelay)
|
||||
}
|
||||
return tls.Client(conn, c.config), nil
|
||||
}
|
||||
|
||||
func (s *STDClientConfig) Clone() Config {
|
||||
return &STDClientConfig{s.config.Clone()}
|
||||
func (c *STDClientConfig) Clone() Config {
|
||||
return &STDClientConfig{c.ctx, c.config.Clone(), c.fragment, c.fragmentFallbackDelay, c.recordFragment}
|
||||
}
|
||||
|
||||
func (c *STDClientConfig) ECHConfigList() []byte {
|
||||
return c.config.EncryptedClientHelloConfigList
|
||||
}
|
||||
|
||||
func (c *STDClientConfig) SetECHConfigList(EncryptedClientHelloConfigList []byte) {
|
||||
c.config.EncryptedClientHelloConfigList = EncryptedClientHelloConfigList
|
||||
}
|
||||
|
||||
func NewSTDClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
||||
@ -60,9 +77,7 @@ func NewSTDClient(ctx context.Context, serverAddress string, options option.Outb
|
||||
var tlsConfig tls.Config
|
||||
tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
|
||||
tlsConfig.RootCAs = adapter.RootPoolFromContext(ctx)
|
||||
if options.DisableSNI {
|
||||
tlsConfig.ServerName = "127.0.0.1"
|
||||
} else {
|
||||
if !options.DisableSNI {
|
||||
tlsConfig.ServerName = serverName
|
||||
}
|
||||
if options.Insecure {
|
||||
@ -127,8 +142,10 @@ func NewSTDClient(ctx context.Context, serverAddress string, options option.Outb
|
||||
}
|
||||
tlsConfig.RootCAs = certPool
|
||||
}
|
||||
stdConfig := &STDClientConfig{ctx, &tlsConfig, options.Fragment, time.Duration(options.FragmentFallbackDelay), options.RecordFragment}
|
||||
if options.ECH != nil && options.ECH.Enabled {
|
||||
return parseECHClientConfig(ctx, options, &tlsConfig)
|
||||
return parseECHClientConfig(ctx, stdConfig, options)
|
||||
} else {
|
||||
return stdConfig, nil
|
||||
}
|
||||
return &STDClientConfig{&tlsConfig}, nil
|
||||
}
|
||||
|
@ -8,11 +8,12 @@ import (
|
||||
"crypto/x509"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/tlsfragment"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/ntp"
|
||||
@ -22,46 +23,60 @@ import (
|
||||
)
|
||||
|
||||
type UTLSClientConfig struct {
|
||||
config *utls.Config
|
||||
id utls.ClientHelloID
|
||||
ctx context.Context
|
||||
config *utls.Config
|
||||
id utls.ClientHelloID
|
||||
fragment bool
|
||||
fragmentFallbackDelay time.Duration
|
||||
recordFragment bool
|
||||
}
|
||||
|
||||
func (e *UTLSClientConfig) ServerName() string {
|
||||
return e.config.ServerName
|
||||
func (c *UTLSClientConfig) ServerName() string {
|
||||
return c.config.ServerName
|
||||
}
|
||||
|
||||
func (e *UTLSClientConfig) SetServerName(serverName string) {
|
||||
e.config.ServerName = serverName
|
||||
func (c *UTLSClientConfig) SetServerName(serverName string) {
|
||||
c.config.ServerName = serverName
|
||||
}
|
||||
|
||||
func (e *UTLSClientConfig) NextProtos() []string {
|
||||
return e.config.NextProtos
|
||||
func (c *UTLSClientConfig) NextProtos() []string {
|
||||
return c.config.NextProtos
|
||||
}
|
||||
|
||||
func (e *UTLSClientConfig) SetNextProtos(nextProto []string) {
|
||||
func (c *UTLSClientConfig) SetNextProtos(nextProto []string) {
|
||||
if len(nextProto) == 1 && nextProto[0] == http2.NextProtoTLS {
|
||||
nextProto = append(nextProto, "http/1.1")
|
||||
}
|
||||
e.config.NextProtos = nextProto
|
||||
c.config.NextProtos = nextProto
|
||||
}
|
||||
|
||||
func (e *UTLSClientConfig) Config() (*STDConfig, error) {
|
||||
func (c *UTLSClientConfig) Config() (*STDConfig, error) {
|
||||
return nil, E.New("unsupported usage for uTLS")
|
||||
}
|
||||
|
||||
func (e *UTLSClientConfig) Client(conn net.Conn) (Conn, error) {
|
||||
return &utlsALPNWrapper{utlsConnWrapper{utls.UClient(conn, e.config.Clone(), e.id)}, e.config.NextProtos}, nil
|
||||
}
|
||||
|
||||
func (e *UTLSClientConfig) SetSessionIDGenerator(generator func(clientHello []byte, sessionID []byte) error) {
|
||||
e.config.SessionIDGenerator = generator
|
||||
}
|
||||
|
||||
func (e *UTLSClientConfig) Clone() Config {
|
||||
return &UTLSClientConfig{
|
||||
config: e.config.Clone(),
|
||||
id: e.id,
|
||||
func (c *UTLSClientConfig) Client(conn net.Conn) (Conn, error) {
|
||||
if c.recordFragment {
|
||||
conn = tf.NewConn(conn, c.ctx, c.fragment, c.recordFragment, c.fragmentFallbackDelay)
|
||||
}
|
||||
return &utlsALPNWrapper{utlsConnWrapper{utls.UClient(conn, c.config.Clone(), c.id)}, c.config.NextProtos}, nil
|
||||
}
|
||||
|
||||
func (c *UTLSClientConfig) SetSessionIDGenerator(generator func(clientHello []byte, sessionID []byte) error) {
|
||||
c.config.SessionIDGenerator = generator
|
||||
}
|
||||
|
||||
func (c *UTLSClientConfig) Clone() Config {
|
||||
return &UTLSClientConfig{
|
||||
c.ctx, c.config.Clone(), c.id, c.fragment, c.fragmentFallbackDelay, c.recordFragment,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *UTLSClientConfig) ECHConfigList() []byte {
|
||||
return c.config.EncryptedClientHelloConfigList
|
||||
}
|
||||
|
||||
func (c *UTLSClientConfig) SetECHConfigList(EncryptedClientHelloConfigList []byte) {
|
||||
c.config.EncryptedClientHelloConfigList = EncryptedClientHelloConfigList
|
||||
}
|
||||
|
||||
type utlsConnWrapper struct {
|
||||
@ -116,14 +131,12 @@ func (c *utlsALPNWrapper) HandshakeContext(ctx context.Context) error {
|
||||
return c.UConn.HandshakeContext(ctx)
|
||||
}
|
||||
|
||||
func NewUTLSClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (*UTLSClientConfig, error) {
|
||||
func NewUTLSClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
||||
var serverName string
|
||||
if options.ServerName != "" {
|
||||
serverName = options.ServerName
|
||||
} else if serverAddress != "" {
|
||||
if _, err := netip.ParseAddr(serverName); err != nil {
|
||||
serverName = serverAddress
|
||||
}
|
||||
serverName = serverAddress
|
||||
}
|
||||
if serverName == "" && !options.Insecure {
|
||||
return nil, E.New("missing server_name or insecure=true")
|
||||
@ -132,11 +145,7 @@ func NewUTLSClient(ctx context.Context, serverAddress string, options option.Out
|
||||
var tlsConfig utls.Config
|
||||
tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
|
||||
tlsConfig.RootCAs = adapter.RootPoolFromContext(ctx)
|
||||
if options.DisableSNI {
|
||||
tlsConfig.ServerName = "127.0.0.1"
|
||||
} else {
|
||||
tlsConfig.ServerName = serverName
|
||||
}
|
||||
tlsConfig.ServerName = serverName
|
||||
if options.Insecure {
|
||||
tlsConfig.InsecureSkipVerify = options.Insecure
|
||||
} else if options.DisableSNI {
|
||||
@ -192,7 +201,15 @@ func NewUTLSClient(ctx context.Context, serverAddress string, options option.Out
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &UTLSClientConfig{&tlsConfig, id}, nil
|
||||
uConfig := &UTLSClientConfig{ctx, &tlsConfig, id, options.Fragment, time.Duration(options.FragmentFallbackDelay), options.RecordFragment}
|
||||
if options.ECH != nil && options.ECH.Enabled {
|
||||
if options.Reality != nil && options.Reality.Enabled {
|
||||
return nil, E.New("Reality is conflict with ECH")
|
||||
}
|
||||
return parseECHClientConfig(ctx, uConfig, options)
|
||||
} else {
|
||||
return uConfig, nil
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
@ -220,7 +237,7 @@ func init() {
|
||||
|
||||
func uTLSClientHelloID(name string) (utls.ClientHelloID, error) {
|
||||
switch name {
|
||||
case "chrome_psk", "chrome_psk_shuffle", "chrome_padding_psk_shuffle", "chrome_pq":
|
||||
case "chrome_psk", "chrome_psk_shuffle", "chrome_padding_psk_shuffle", "chrome_pq", "chrome_pq_psk":
|
||||
fallthrough
|
||||
case "chrome", "":
|
||||
return utls.HelloChrome_Auto, nil
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
|
||||
"golang.org/x/net/publicsuffix"
|
||||
@ -19,16 +20,21 @@ type Conn struct {
|
||||
tcpConn *net.TCPConn
|
||||
ctx context.Context
|
||||
firstPacketWritten bool
|
||||
splitPacket bool
|
||||
splitRecord bool
|
||||
fallbackDelay time.Duration
|
||||
}
|
||||
|
||||
func NewConn(conn net.Conn, ctx context.Context, splitRecord bool, fallbackDelay time.Duration) *Conn {
|
||||
func NewConn(conn net.Conn, ctx context.Context, splitPacket bool, splitRecord bool, fallbackDelay time.Duration) *Conn {
|
||||
if fallbackDelay == 0 {
|
||||
fallbackDelay = C.TLSFragmentFallbackDelay
|
||||
}
|
||||
tcpConn, _ := N.UnwrapReader(conn).(*net.TCPConn)
|
||||
return &Conn{
|
||||
Conn: conn,
|
||||
tcpConn: tcpConn,
|
||||
ctx: ctx,
|
||||
splitPacket: splitPacket,
|
||||
splitRecord: splitRecord,
|
||||
fallbackDelay: fallbackDelay,
|
||||
}
|
||||
@ -41,7 +47,7 @@ func (c *Conn) Write(b []byte) (n int, err error) {
|
||||
}()
|
||||
serverName := indexTLSServerName(b)
|
||||
if serverName != nil {
|
||||
if !c.splitRecord {
|
||||
if c.splitPacket {
|
||||
if c.tcpConn != nil {
|
||||
err = c.tcpConn.SetNoDelay(true)
|
||||
if err != nil {
|
||||
@ -81,33 +87,41 @@ func (c *Conn) Write(b []byte) (n int, err error) {
|
||||
payload = b[splitIndexes[i-1]:splitIndexes[i]]
|
||||
}
|
||||
if c.splitRecord {
|
||||
if c.splitPacket {
|
||||
buffer.Reset()
|
||||
}
|
||||
payloadLen := uint16(len(payload))
|
||||
buffer.Write(b[:3])
|
||||
binary.Write(&buffer, binary.BigEndian, payloadLen)
|
||||
buffer.Write(payload)
|
||||
} else if c.tcpConn != nil && i != len(splitIndexes) {
|
||||
err = writeAndWaitAck(c.ctx, c.tcpConn, payload, c.fallbackDelay)
|
||||
if err != nil {
|
||||
return
|
||||
if c.splitPacket {
|
||||
payload = buffer.Bytes()
|
||||
}
|
||||
} else {
|
||||
_, err = c.Conn.Write(payload)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if c.splitPacket {
|
||||
if c.tcpConn != nil && i != len(splitIndexes) {
|
||||
err = writeAndWaitAck(c.ctx, c.tcpConn, payload, c.fallbackDelay)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
_, err = c.Conn.Write(payload)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if c.splitRecord {
|
||||
if c.splitRecord && !c.splitPacket {
|
||||
_, err = c.Conn.Write(buffer.Bytes())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if c.tcpConn != nil {
|
||||
err = c.tcpConn.SetNoDelay(false)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
if c.tcpConn != nil {
|
||||
err = c.tcpConn.SetNoDelay(false)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return len(b), nil
|
||||
|
@ -15,7 +15,7 @@ func TestTLSFragment(t *testing.T) {
|
||||
t.Parallel()
|
||||
tcpConn, err := net.Dial("tcp", "1.1.1.1:443")
|
||||
require.NoError(t, err)
|
||||
tlsConn := tls.Client(tf.NewConn(tcpConn, context.Background(), false, 0), &tls.Config{
|
||||
tlsConn := tls.Client(tf.NewConn(tcpConn, context.Background(), true, false, 0), &tls.Config{
|
||||
ServerName: "www.cloudflare.com",
|
||||
})
|
||||
require.NoError(t, tlsConn.Handshake())
|
||||
@ -25,7 +25,17 @@ func TestTLSRecordFragment(t *testing.T) {
|
||||
t.Parallel()
|
||||
tcpConn, err := net.Dial("tcp", "1.1.1.1:443")
|
||||
require.NoError(t, err)
|
||||
tlsConn := tls.Client(tf.NewConn(tcpConn, context.Background(), true, 0), &tls.Config{
|
||||
tlsConn := tls.Client(tf.NewConn(tcpConn, context.Background(), false, true, 0), &tls.Config{
|
||||
ServerName: "www.cloudflare.com",
|
||||
})
|
||||
require.NoError(t, tlsConn.Handshake())
|
||||
}
|
||||
|
||||
func TestTLS2Fragment(t *testing.T) {
|
||||
t.Parallel()
|
||||
tcpConn, err := net.Dial("tcp", "1.1.1.1:443")
|
||||
require.NoError(t, err)
|
||||
tlsConn := tls.Client(tf.NewConn(tcpConn, context.Background(), true, true, 0), &tls.Config{
|
||||
ServerName: "www.cloudflare.com",
|
||||
})
|
||||
require.NoError(t, tlsConn.Handshake())
|
||||
|
@ -2,10 +2,37 @@
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
#### 1.12.0-beta.22
|
||||
#### 1.12.0-beta.24
|
||||
|
||||
* Allow `tls_fragment` and `tls_record_fragment` to be enabled together **1**
|
||||
* Also add fragment options for TLS client configuration **2**
|
||||
|
||||
**1**:
|
||||
|
||||
For debugging only, it is recommended to disable if record fragmentation works.
|
||||
|
||||
See [Route Action](/configuration/route/rule_action/#tls_fragment).
|
||||
|
||||
**2**:
|
||||
|
||||
See [TLS](/configuration/shared/tls/).
|
||||
|
||||
#### 1.12.0-beta.23
|
||||
|
||||
* Add loopback address support for tun **1**
|
||||
* Add cache support for ssm-api **2**
|
||||
* Fixes and improvements
|
||||
|
||||
**1**:
|
||||
|
||||
TUN now implements SideStore's StosVPN.
|
||||
|
||||
See [Tun](/configuration/inbound/tun/#loopback_address).
|
||||
|
||||
**2**:
|
||||
|
||||
See [SSM API Service](/configuration/service/ssm-api/#cache_path).
|
||||
|
||||
#### 1.12.0-beta.21
|
||||
|
||||
* Fix missing `home` option for DERP service **1**
|
||||
|
@ -1,7 +1,11 @@
|
||||
---
|
||||
icon: material/alert-decagram
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "Changes in sing-box 1.12.0"
|
||||
|
||||
:material-plus: [loopback_address](#loopback_address)
|
||||
|
||||
!!! quote "Changes in sing-box 1.11.0"
|
||||
|
||||
:material-delete-alert: [gso](#gso)
|
||||
@ -56,9 +60,12 @@ icon: material/alert-decagram
|
||||
"auto_route": true,
|
||||
"iproute2_table_index": 2022,
|
||||
"iproute2_rule_index": 9000,
|
||||
"auto_redirect": false,
|
||||
"auto_redirect": true,
|
||||
"auto_redirect_input_mark": "0x2023",
|
||||
"auto_redirect_output_mark": "0x2024",
|
||||
"loopback_address": [
|
||||
"10.0.7.1"
|
||||
],
|
||||
"strict_route": true,
|
||||
"route_address": [
|
||||
"0.0.0.0/1",
|
||||
@ -66,7 +73,6 @@ icon: material/alert-decagram
|
||||
"::/1",
|
||||
"8000::/1"
|
||||
],
|
||||
|
||||
"route_exclude_address": [
|
||||
"192.168.0.0/16",
|
||||
"fc00::/7"
|
||||
@ -117,7 +123,6 @@ icon: material/alert-decagram
|
||||
"match_domain": []
|
||||
}
|
||||
},
|
||||
|
||||
// Deprecated
|
||||
"gso": false,
|
||||
"inet4_address": [
|
||||
@ -140,8 +145,8 @@ icon: material/alert-decagram
|
||||
"inet6_route_exclude_address": [
|
||||
"fc00::/7"
|
||||
],
|
||||
|
||||
... // Listen Fields
|
||||
...
|
||||
// Listen Fields
|
||||
}
|
||||
```
|
||||
|
||||
@ -273,6 +278,16 @@ Connection output mark used by `auto_redirect`.
|
||||
|
||||
`0x2024` is used by default.
|
||||
|
||||
#### loopback_address
|
||||
|
||||
!!! question "Since sing-box 1.12.0"
|
||||
|
||||
Loopback addresses make TCP connections to the specified address connect to the source address.
|
||||
|
||||
Setting option value to `10.0.7.1` achieves the same behavior as SideStore/StosVPN.
|
||||
|
||||
When `auto_redirect` is enabled, the same behavior can be achieved for LAN devices (not just local) as a gateway.
|
||||
|
||||
#### strict_route
|
||||
|
||||
Enforce strict routing rules when `auto_route` is enabled:
|
||||
|
@ -1,7 +1,11 @@
|
||||
---
|
||||
icon: material/alert-decagram
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "sing-box 1.12.0 中的更改"
|
||||
|
||||
:material-plus: [loopback_address](#loopback_address)
|
||||
|
||||
!!! quote "sing-box 1.11.0 中的更改"
|
||||
|
||||
:material-delete-alert: [gso](#gso)
|
||||
@ -56,9 +60,12 @@ icon: material/alert-decagram
|
||||
"auto_route": true,
|
||||
"iproute2_table_index": 2022,
|
||||
"iproute2_rule_index": 9000,
|
||||
"auto_redirect": false,
|
||||
"auto_redirect": true,
|
||||
"auto_redirect_input_mark": "0x2023",
|
||||
"auto_redirect_output_mark": "0x2024",
|
||||
"loopback_address": [
|
||||
"10.0.7.1"
|
||||
],
|
||||
"strict_route": true,
|
||||
"route_address": [
|
||||
"0.0.0.0/1",
|
||||
@ -270,6 +277,16 @@ tun 接口的 IPv6 前缀。
|
||||
|
||||
默认使用 `0x2024`。
|
||||
|
||||
#### loopback_address
|
||||
|
||||
!!! question "自 sing-box 1.12.0 起"
|
||||
|
||||
环回地址是用于使指向指定地址的 TCP 连接连接到来源地址的。
|
||||
|
||||
将选项值设置为 `10.0.7.1` 可实现与 SideStore/StosVPN 相同的行为。
|
||||
|
||||
当启用 `auto_redirect` 时,可以作为网关为局域网设备(而不仅仅是本地)实现相同的行为。
|
||||
|
||||
#### strict_route
|
||||
|
||||
当启用 `auto_route` 时,强制执行严格的路由规则:
|
||||
|
@ -172,14 +172,12 @@ and should not be used to circumvent real censorship.
|
||||
Due to poor performance, try `tls_record_fragment` first, and only apply to server names known to be blocked.
|
||||
|
||||
On Linux, Apple platforms, (administrator privileges required) Windows,
|
||||
the wait time can be automatically detected, otherwise it will fall back to
|
||||
the wait time can be automatically detected. Otherwise, it will fall back to
|
||||
waiting for a fixed time specified by `tls_fragment_fallback_delay`.
|
||||
|
||||
In addition, if the actual wait time is less than 20ms, it will also fall back to waiting for a fixed time,
|
||||
because the target is considered to be local or behind a transparent proxy.
|
||||
|
||||
Conflict with `tls_record_fragment`.
|
||||
|
||||
#### tls_fragment_fallback_delay
|
||||
|
||||
!!! question "Since sing-box 1.12.0"
|
||||
@ -194,11 +192,6 @@ The fallback value used when TLS segmentation cannot automatically determine the
|
||||
|
||||
Fragment TLS handshake into multiple TLS records to bypass firewalls.
|
||||
|
||||
This feature is intended to circumvent simple firewalls based on **plaintext packet matching**,
|
||||
and should not be used to circumvent real censorship.
|
||||
|
||||
Conflict with `tls_fragment`.
|
||||
|
||||
### sniff
|
||||
|
||||
```json
|
||||
|
@ -170,8 +170,6 @@ UDP 连接超时时间。
|
||||
|
||||
此外,若实际等待时间小于 20 毫秒,同样会回退至固定等待时间模式,因为此时判定目标处于本地或透明代理之后。
|
||||
|
||||
与 `tls_record_fragment` 冲突。
|
||||
|
||||
#### tls_fragment_fallback_delay
|
||||
|
||||
!!! question "自 sing-box 1.12.0 起"
|
||||
@ -186,10 +184,6 @@ UDP 连接超时时间。
|
||||
|
||||
通过分段 TLS 握手数据包到多个 TLS 记录来绕过防火墙检测。
|
||||
|
||||
此功能旨在规避基于**明文数据包匹配**的简单防火墙,不应该用于规避真的审查。
|
||||
|
||||
与 `tls_fragment` 冲突。
|
||||
|
||||
### sniff
|
||||
|
||||
```json
|
||||
|
@ -19,6 +19,7 @@ See https://github.com/Shadowsocks-NET/shadowsocks-specs/blob/main/2023-1-shadow
|
||||
... // Listen Fields
|
||||
|
||||
"servers": {},
|
||||
"cache_path": "",
|
||||
"tls": {}
|
||||
}
|
||||
```
|
||||
@ -37,7 +38,7 @@ A mapping Object from HTTP endpoints to [Shadowsocks Inbound](/configuration/inb
|
||||
|
||||
Selected Shadowsocks inbounds must be configured with [managed](/configuration/inbound/shadowsocks#managed) enabled.
|
||||
|
||||
Example:
|
||||
Example:
|
||||
|
||||
```json
|
||||
{
|
||||
@ -47,6 +48,11 @@ Example:
|
||||
}
|
||||
```
|
||||
|
||||
#### cache_path
|
||||
|
||||
If set, when the server is about to stop, traffic and user state will be saved to the specified JSON file
|
||||
to be restored on the next startup.
|
||||
|
||||
#### tls
|
||||
|
||||
TLS configuration, see [TLS](/configuration/shared/tls/#inbound).
|
||||
|
@ -4,6 +4,9 @@ icon: material/alert-decagram
|
||||
|
||||
!!! quote "Changes in sing-box 1.12.0"
|
||||
|
||||
:material-plus: [fragment](#fragment)
|
||||
:material-plus: [fragment_fallback_delay](#fragment_fallback_delay)
|
||||
:material-plus: [record_fragment](#record_fragment)
|
||||
:material-delete-clock: [ech.pq_signature_schemes_enabled](#pq_signature_schemes_enabled)
|
||||
:material-delete-clock: [ech.dynamic_record_sizing_disabled](#dynamic_record_sizing_disabled)
|
||||
|
||||
@ -82,6 +85,9 @@ icon: material/alert-decagram
|
||||
"cipher_suites": [],
|
||||
"certificate": "",
|
||||
"certificate_path": "",
|
||||
"fragment": false,
|
||||
"fragment_fallback_delay": "",
|
||||
"record_fragment": false,
|
||||
"ech": {
|
||||
"enabled": false,
|
||||
"config": [],
|
||||
@ -313,6 +319,44 @@ The path to ECH configuration, in PEM format.
|
||||
|
||||
If empty, load from DNS will be attempted.
|
||||
|
||||
#### fragment
|
||||
|
||||
!!! question "Since sing-box 1.12.0"
|
||||
|
||||
==Client only==
|
||||
|
||||
Fragment TLS handshakes to bypass firewalls.
|
||||
|
||||
This feature is intended to circumvent simple firewalls based on **plaintext packet matching**,
|
||||
and should not be used to circumvent real censorship.
|
||||
|
||||
Due to poor performance, try `record_fragment` first, and only apply to server names known to be blocked.
|
||||
|
||||
On Linux, Apple platforms, (administrator privileges required) Windows,
|
||||
the wait time can be automatically detected. Otherwise, it will fall back to
|
||||
waiting for a fixed time specified by `fragment_fallback_delay`.
|
||||
|
||||
In addition, if the actual wait time is less than 20ms, it will also fall back to waiting for a fixed time,
|
||||
because the target is considered to be local or behind a transparent proxy.
|
||||
|
||||
#### fragment_fallback_delay
|
||||
|
||||
!!! question "Since sing-box 1.12.0"
|
||||
|
||||
==Client only==
|
||||
|
||||
The fallback value used when TLS segmentation cannot automatically determine the wait time.
|
||||
|
||||
`500ms` is used by default.
|
||||
|
||||
#### record_fragment
|
||||
|
||||
!!! question "Since sing-box 1.12.0"
|
||||
|
||||
==Client only==
|
||||
|
||||
Fragment TLS handshake into multiple TLS records to bypass firewalls.
|
||||
|
||||
### ACME Fields
|
||||
|
||||
#### domain
|
||||
|
@ -4,6 +4,9 @@ icon: material/alert-decagram
|
||||
|
||||
!!! quote "sing-box 1.12.0 中的更改"
|
||||
|
||||
:material-plus: [tls_fragment](#tls_fragment)
|
||||
:material-plus: [tls_fragment_fallback_delay](#tls_fragment_fallback_delay)
|
||||
:material-plus: [tls_record_fragment](#tls_record_fragment)
|
||||
:material-delete-clock: [ech.pq_signature_schemes_enabled](#pq_signature_schemes_enabled)
|
||||
:material-delete-clock: [ech.dynamic_record_sizing_disabled](#dynamic_record_sizing_disabled)
|
||||
|
||||
@ -82,6 +85,9 @@ icon: material/alert-decagram
|
||||
"cipher_suites": [],
|
||||
"certificate": [],
|
||||
"certificate_path": "",
|
||||
"fragment": false,
|
||||
"fragment_fallback_delay": "",
|
||||
"record_fragment": false,
|
||||
"ech": {
|
||||
"enabled": false,
|
||||
"pq_signature_schemes_enabled": false,
|
||||
@ -305,6 +311,41 @@ ECH PEM 配置路径
|
||||
如果为 true,则始终使用最大可能的 TLS 记录大小。
|
||||
如果为 false,则可能会调整 TLS 记录的大小以尝试改善延迟。
|
||||
|
||||
#### tls_fragment
|
||||
|
||||
!!! question "自 sing-box 1.12.0 起"
|
||||
|
||||
==仅客户端==
|
||||
|
||||
通过分段 TLS 握手数据包来绕过防火墙检测。
|
||||
|
||||
此功能旨在规避基于**明文数据包匹配**的简单防火墙,不应该用于规避真的审查。
|
||||
|
||||
由于性能不佳,请首先尝试 `tls_record_fragment`,且仅应用于已知被阻止的服务器名称。
|
||||
|
||||
在 Linux、Apple 平台和需要管理员权限的 Windows 系统上,可自动检测等待时间。
|
||||
若无法自动检测,将回退使用 `tls_fragment_fallback_delay` 指定的固定等待时间。
|
||||
|
||||
此外,若实际等待时间小于 20 毫秒,同样会回退至固定等待时间模式,因为此时判定目标处于本地或透明代理之后。
|
||||
|
||||
#### tls_fragment_fallback_delay
|
||||
|
||||
!!! question "自 sing-box 1.12.0 起"
|
||||
|
||||
==仅客户端==
|
||||
|
||||
当 TLS 分片功能无法自动判定等待时间时使用的回退值。
|
||||
|
||||
默认使用 `500ms`。
|
||||
|
||||
#### tls_record_fragment
|
||||
|
||||
==仅客户端==
|
||||
|
||||
!!! question "自 sing-box 1.12.0 起"
|
||||
|
||||
通过分段 TLS 握手数据包到多个 TLS 记录来绕过防火墙检测。
|
||||
|
||||
### ACME 字段
|
||||
|
||||
#### domain
|
||||
|
2
go.mod
2
go.mod
@ -34,7 +34,7 @@ require (
|
||||
github.com/sagernet/sing-shadowsocks v0.2.8
|
||||
github.com/sagernet/sing-shadowsocks2 v0.2.1
|
||||
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11
|
||||
github.com/sagernet/sing-tun v0.6.6-0.20250428031943-0686f8c4f210
|
||||
github.com/sagernet/sing-tun v0.6.6-0.20250610083027-da0a50057fb5
|
||||
github.com/sagernet/sing-vmess v0.2.4-0.20250605032146-38cc72672c88
|
||||
github.com/sagernet/smux v1.5.34-mod.2
|
||||
github.com/sagernet/tailscale v1.80.3-mod.5
|
||||
|
4
go.sum
4
go.sum
@ -180,8 +180,8 @@ github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnq
|
||||
github.com/sagernet/sing-shadowsocks2 v0.2.1/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ=
|
||||
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 h1:tK+75l64tm9WvEFrYRE1t0YxoFdWQqw/h7Uhzj0vJ+w=
|
||||
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA=
|
||||
github.com/sagernet/sing-tun v0.6.6-0.20250428031943-0686f8c4f210 h1:6H4BZaTqKI3YcDMyTV3E576LuJM4S4wY99xoq2T1ECw=
|
||||
github.com/sagernet/sing-tun v0.6.6-0.20250428031943-0686f8c4f210/go.mod h1:fisFCbC4Vfb6HqQNcwPJi2CDK2bf0Xapyz3j3t4cnHE=
|
||||
github.com/sagernet/sing-tun v0.6.6-0.20250610083027-da0a50057fb5 h1:zlcioVa11g8VLz5L0yPG7PbvQrw7mrxkDDdlMPEgqDk=
|
||||
github.com/sagernet/sing-tun v0.6.6-0.20250610083027-da0a50057fb5/go.mod h1:fisFCbC4Vfb6HqQNcwPJi2CDK2bf0Xapyz3j3t4cnHE=
|
||||
github.com/sagernet/sing-vmess v0.2.4-0.20250605032146-38cc72672c88 h1:0pVm8sPOel+BoiCddW3pV3cKDKEaSioVTYDdTSKjyFI=
|
||||
github.com/sagernet/sing-vmess v0.2.4-0.20250605032146-38cc72672c88/go.mod h1:IL8Rr+EGwuqijszZkNrEFTQDKhilEpkqFqOlvdpS6/w=
|
||||
github.com/sagernet/smux v1.5.34-mod.2 h1:gkmBjIjlJ2zQKpLigOkFur5kBKdV6bNRoFu2WkltRQ4=
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
|
||||
type SSMAPIServiceOptions struct {
|
||||
ListenOptions
|
||||
Servers *badjson.TypedMap[string, string] `json:"servers"`
|
||||
Servers *badjson.TypedMap[string, string] `json:"servers"`
|
||||
CachePath string `json:"cache_path,omitempty"`
|
||||
InboundTLSOptionsContainer
|
||||
}
|
||||
|
@ -37,19 +37,22 @@ func (o *InboundTLSOptionsContainer) ReplaceInboundTLSOptions(options *InboundTL
|
||||
}
|
||||
|
||||
type OutboundTLSOptions struct {
|
||||
Enabled bool `json:"enabled,omitempty"`
|
||||
DisableSNI bool `json:"disable_sni,omitempty"`
|
||||
ServerName string `json:"server_name,omitempty"`
|
||||
Insecure bool `json:"insecure,omitempty"`
|
||||
ALPN badoption.Listable[string] `json:"alpn,omitempty"`
|
||||
MinVersion string `json:"min_version,omitempty"`
|
||||
MaxVersion string `json:"max_version,omitempty"`
|
||||
CipherSuites badoption.Listable[string] `json:"cipher_suites,omitempty"`
|
||||
Certificate badoption.Listable[string] `json:"certificate,omitempty"`
|
||||
CertificatePath string `json:"certificate_path,omitempty"`
|
||||
ECH *OutboundECHOptions `json:"ech,omitempty"`
|
||||
UTLS *OutboundUTLSOptions `json:"utls,omitempty"`
|
||||
Reality *OutboundRealityOptions `json:"reality,omitempty"`
|
||||
Enabled bool `json:"enabled,omitempty"`
|
||||
DisableSNI bool `json:"disable_sni,omitempty"`
|
||||
ServerName string `json:"server_name,omitempty"`
|
||||
Insecure bool `json:"insecure,omitempty"`
|
||||
ALPN badoption.Listable[string] `json:"alpn,omitempty"`
|
||||
MinVersion string `json:"min_version,omitempty"`
|
||||
MaxVersion string `json:"max_version,omitempty"`
|
||||
CipherSuites badoption.Listable[string] `json:"cipher_suites,omitempty"`
|
||||
Certificate badoption.Listable[string] `json:"certificate,omitempty"`
|
||||
CertificatePath string `json:"certificate_path,omitempty"`
|
||||
Fragment bool `json:"fragment,omitempty"`
|
||||
FragmentFallbackDelay badoption.Duration `json:"fragment_fallback_delay,omitempty"`
|
||||
RecordFragment bool `json:"record_fragment,omitempty"`
|
||||
ECH *OutboundECHOptions `json:"ech,omitempty"`
|
||||
UTLS *OutboundUTLSOptions `json:"utls,omitempty"`
|
||||
Reality *OutboundRealityOptions `json:"reality,omitempty"`
|
||||
}
|
||||
|
||||
type OutboundTLSOptionsContainer struct {
|
||||
|
@ -20,6 +20,7 @@ type TunInboundOptions struct {
|
||||
AutoRedirect bool `json:"auto_redirect,omitempty"`
|
||||
AutoRedirectInputMark FwMark `json:"auto_redirect_input_mark,omitempty"`
|
||||
AutoRedirectOutputMark FwMark `json:"auto_redirect_output_mark,omitempty"`
|
||||
LoopbackAddress badoption.Listable[netip.Addr] `json:"loopback_address,omitempty"`
|
||||
StrictRoute bool `json:"strict_route,omitempty"`
|
||||
RouteAddress badoption.Listable[netip.Prefix] `json:"route_address,omitempty"`
|
||||
RouteAddressSet badoption.Listable[string] `json:"route_address_set,omitempty"`
|
||||
|
@ -190,6 +190,8 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
|
||||
IPRoute2RuleIndex: ruleIndex,
|
||||
AutoRedirectInputMark: inputMark,
|
||||
AutoRedirectOutputMark: outputMark,
|
||||
Inet4LoopbackAddress: common.Filter(options.LoopbackAddress, netip.Addr.Is4),
|
||||
Inet6LoopbackAddress: common.Filter(options.LoopbackAddress, netip.Addr.Is6),
|
||||
StrictRoute: options.StrictRoute,
|
||||
IncludeInterface: options.IncludeInterface,
|
||||
ExcludeInterface: options.ExcludeInterface,
|
||||
|
@ -90,14 +90,8 @@ func (m *ConnectionManager) NewConnection(ctx context.Context, this N.Dialer, co
|
||||
m.logger.ErrorContext(ctx, err)
|
||||
return
|
||||
}
|
||||
if metadata.TLSFragment {
|
||||
fallbackDelay := metadata.TLSFragmentFallbackDelay
|
||||
if fallbackDelay == 0 {
|
||||
fallbackDelay = C.TLSFragmentFallbackDelay
|
||||
}
|
||||
remoteConn = tf.NewConn(remoteConn, ctx, false, fallbackDelay)
|
||||
} else if metadata.TLSRecordFragment {
|
||||
remoteConn = tf.NewConn(remoteConn, ctx, true, 0)
|
||||
if metadata.TLSFragment || metadata.TLSRecordFragment {
|
||||
remoteConn = tf.NewConn(remoteConn, ctx, metadata.TLSFragment, metadata.TLSRecordFragment, metadata.TLSFragmentFallbackDelay)
|
||||
}
|
||||
m.access.Lock()
|
||||
element := m.connections.PushBack(conn)
|
||||
|
@ -134,7 +134,7 @@ func NewService(ctx context.Context, logger log.ContextLogger, tag string, optio
|
||||
func (d *Service) Start(stage adapter.StartStage) error {
|
||||
switch stage {
|
||||
case adapter.StartStateStart:
|
||||
config, err := readDERPConfig(d.configPath)
|
||||
config, err := readDERPConfig(filemanager.BasePath(d.ctx, d.configPath))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
222
service/ssmapi/cache.go
Normal file
222
service/ssmapi/cache.go
Normal file
@ -0,0 +1,222 @@
|
||||
package ssmapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
|
||||
"github.com/sagernet/sing/common/atomic"
|
||||
"github.com/sagernet/sing/common/json"
|
||||
"github.com/sagernet/sing/common/json/badjson"
|
||||
"github.com/sagernet/sing/service/filemanager"
|
||||
)
|
||||
|
||||
type Cache struct {
|
||||
Endpoints *badjson.TypedMap[string, *EndpointCache] `json:"endpoints"`
|
||||
}
|
||||
|
||||
type EndpointCache struct {
|
||||
GlobalUplink int64 `json:"global_uplink"`
|
||||
GlobalDownlink int64 `json:"global_downlink"`
|
||||
GlobalUplinkPackets int64 `json:"global_uplink_packets"`
|
||||
GlobalDownlinkPackets int64 `json:"global_downlink_packets"`
|
||||
GlobalTCPSessions int64 `json:"global_tcp_sessions"`
|
||||
GlobalUDPSessions int64 `json:"global_udp_sessions"`
|
||||
UserUplink *badjson.TypedMap[string, int64] `json:"user_uplink"`
|
||||
UserDownlink *badjson.TypedMap[string, int64] `json:"user_downlink"`
|
||||
UserUplinkPackets *badjson.TypedMap[string, int64] `json:"user_uplink_packets"`
|
||||
UserDownlinkPackets *badjson.TypedMap[string, int64] `json:"user_downlink_packets"`
|
||||
UserTCPSessions *badjson.TypedMap[string, int64] `json:"user_tcp_sessions"`
|
||||
UserUDPSessions *badjson.TypedMap[string, int64] `json:"user_udp_sessions"`
|
||||
Users *badjson.TypedMap[string, string] `json:"users"`
|
||||
}
|
||||
|
||||
func (s *Service) loadCache() error {
|
||||
if s.cachePath == "" {
|
||||
return nil
|
||||
}
|
||||
basePath := filemanager.BasePath(s.ctx, s.cachePath)
|
||||
cacheBinary, err := os.ReadFile(basePath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
err = s.decodeCache(cacheBinary)
|
||||
if err != nil {
|
||||
os.RemoveAll(basePath)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) saveCache() error {
|
||||
if s.cachePath == "" {
|
||||
return nil
|
||||
}
|
||||
basePath := filemanager.BasePath(s.ctx, s.cachePath)
|
||||
err := os.MkdirAll(filepath.Dir(basePath), 0o777)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cacheBinary, err := s.encodeCache()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(s.cachePath, cacheBinary, 0o644)
|
||||
}
|
||||
|
||||
func (s *Service) decodeCache(cacheBinary []byte) error {
|
||||
if len(cacheBinary) == 0 {
|
||||
return nil
|
||||
}
|
||||
cache, err := json.UnmarshalExtended[*Cache](cacheBinary)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cache.Endpoints == nil || cache.Endpoints.Size() == 0 {
|
||||
return nil
|
||||
}
|
||||
for _, entry := range cache.Endpoints.Entries() {
|
||||
trafficManager, loaded := s.traffics[entry.Key]
|
||||
if !loaded {
|
||||
continue
|
||||
}
|
||||
trafficManager.globalUplink.Store(entry.Value.GlobalUplink)
|
||||
trafficManager.globalDownlink.Store(entry.Value.GlobalDownlink)
|
||||
trafficManager.globalUplinkPackets.Store(entry.Value.GlobalUplinkPackets)
|
||||
trafficManager.globalDownlinkPackets.Store(entry.Value.GlobalDownlinkPackets)
|
||||
trafficManager.globalTCPSessions.Store(entry.Value.GlobalTCPSessions)
|
||||
trafficManager.globalUDPSessions.Store(entry.Value.GlobalUDPSessions)
|
||||
trafficManager.userUplink = typedAtomicInt64Map(entry.Value.UserUplink)
|
||||
trafficManager.userDownlink = typedAtomicInt64Map(entry.Value.UserDownlink)
|
||||
trafficManager.userUplinkPackets = typedAtomicInt64Map(entry.Value.UserUplinkPackets)
|
||||
trafficManager.userDownlinkPackets = typedAtomicInt64Map(entry.Value.UserDownlinkPackets)
|
||||
trafficManager.userTCPSessions = typedAtomicInt64Map(entry.Value.UserTCPSessions)
|
||||
trafficManager.userUDPSessions = typedAtomicInt64Map(entry.Value.UserUDPSessions)
|
||||
userManager, loaded := s.users[entry.Key]
|
||||
if !loaded {
|
||||
continue
|
||||
}
|
||||
userManager.usersMap = typedMap(entry.Value.Users)
|
||||
_ = userManager.postUpdate(false)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) encodeCache() ([]byte, error) {
|
||||
endpoints := new(badjson.TypedMap[string, *EndpointCache])
|
||||
for tag, traffic := range s.traffics {
|
||||
var (
|
||||
userUplink = new(badjson.TypedMap[string, int64])
|
||||
userDownlink = new(badjson.TypedMap[string, int64])
|
||||
userUplinkPackets = new(badjson.TypedMap[string, int64])
|
||||
userDownlinkPackets = new(badjson.TypedMap[string, int64])
|
||||
userTCPSessions = new(badjson.TypedMap[string, int64])
|
||||
userUDPSessions = new(badjson.TypedMap[string, int64])
|
||||
userMap = new(badjson.TypedMap[string, string])
|
||||
)
|
||||
for user, uplink := range traffic.userUplink {
|
||||
if uplink.Load() > 0 {
|
||||
userUplink.Put(user, uplink.Load())
|
||||
}
|
||||
}
|
||||
for user, downlink := range traffic.userDownlink {
|
||||
if downlink.Load() > 0 {
|
||||
userDownlink.Put(user, downlink.Load())
|
||||
}
|
||||
}
|
||||
for user, uplinkPackets := range traffic.userUplinkPackets {
|
||||
if uplinkPackets.Load() > 0 {
|
||||
userUplinkPackets.Put(user, uplinkPackets.Load())
|
||||
}
|
||||
}
|
||||
for user, downlinkPackets := range traffic.userDownlinkPackets {
|
||||
if downlinkPackets.Load() > 0 {
|
||||
userDownlinkPackets.Put(user, downlinkPackets.Load())
|
||||
}
|
||||
}
|
||||
for user, tcpSessions := range traffic.userTCPSessions {
|
||||
if tcpSessions.Load() > 0 {
|
||||
userTCPSessions.Put(user, tcpSessions.Load())
|
||||
}
|
||||
}
|
||||
for user, udpSessions := range traffic.userUDPSessions {
|
||||
if udpSessions.Load() > 0 {
|
||||
userUDPSessions.Put(user, udpSessions.Load())
|
||||
}
|
||||
}
|
||||
userManager := s.users[tag]
|
||||
if userManager != nil && len(userManager.usersMap) > 0 {
|
||||
userMap = new(badjson.TypedMap[string, string])
|
||||
for username, password := range userManager.usersMap {
|
||||
if username != "" && password != "" {
|
||||
userMap.Put(username, password)
|
||||
}
|
||||
}
|
||||
}
|
||||
endpoints.Put(tag, &EndpointCache{
|
||||
GlobalUplink: traffic.globalUplink.Load(),
|
||||
GlobalDownlink: traffic.globalDownlink.Load(),
|
||||
GlobalUplinkPackets: traffic.globalUplinkPackets.Load(),
|
||||
GlobalDownlinkPackets: traffic.globalDownlinkPackets.Load(),
|
||||
GlobalTCPSessions: traffic.globalTCPSessions.Load(),
|
||||
GlobalUDPSessions: traffic.globalUDPSessions.Load(),
|
||||
UserUplink: sortTypedMap(userUplink),
|
||||
UserDownlink: sortTypedMap(userDownlink),
|
||||
UserUplinkPackets: sortTypedMap(userUplinkPackets),
|
||||
UserDownlinkPackets: sortTypedMap(userDownlinkPackets),
|
||||
UserTCPSessions: sortTypedMap(userTCPSessions),
|
||||
UserUDPSessions: sortTypedMap(userUDPSessions),
|
||||
Users: sortTypedMap(userMap),
|
||||
})
|
||||
}
|
||||
var buffer bytes.Buffer
|
||||
encoder := json.NewEncoder(&buffer)
|
||||
encoder.SetIndent("", " ")
|
||||
err := encoder.Encode(&Cache{
|
||||
Endpoints: sortTypedMap(endpoints),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buffer.Bytes(), nil
|
||||
}
|
||||
|
||||
func sortTypedMap[T comparable](trafficMap *badjson.TypedMap[string, T]) *badjson.TypedMap[string, T] {
|
||||
if trafficMap == nil {
|
||||
return nil
|
||||
}
|
||||
keys := trafficMap.Keys()
|
||||
sort.Strings(keys)
|
||||
sortedMap := new(badjson.TypedMap[string, T])
|
||||
for _, key := range keys {
|
||||
value, _ := trafficMap.Get(key)
|
||||
sortedMap.Put(key, value)
|
||||
}
|
||||
return sortedMap
|
||||
}
|
||||
|
||||
func typedAtomicInt64Map(trafficMap *badjson.TypedMap[string, int64]) map[string]*atomic.Int64 {
|
||||
result := make(map[string]*atomic.Int64)
|
||||
if trafficMap != nil {
|
||||
for _, entry := range trafficMap.Entries() {
|
||||
counter := new(atomic.Int64)
|
||||
counter.Store(entry.Value)
|
||||
result[entry.Key] = counter
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func typedMap[T comparable](trafficMap *badjson.TypedMap[string, T]) map[string]T {
|
||||
result := make(map[string]T)
|
||||
if trafficMap != nil {
|
||||
for _, entry := range trafficMap.Entries() {
|
||||
result[entry.Key] = entry.Value
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
@ -33,6 +33,9 @@ type Service struct {
|
||||
listener *listener.Listener
|
||||
tlsConfig tls.ServerConfig
|
||||
httpServer *http.Server
|
||||
traffics map[string]*TrafficManager
|
||||
users map[string]*UserManager
|
||||
cachePath string
|
||||
}
|
||||
|
||||
func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.SSMAPIServiceOptions) (adapter.Service, error) {
|
||||
@ -50,6 +53,9 @@ func NewService(ctx context.Context, logger log.ContextLogger, tag string, optio
|
||||
httpServer: &http.Server{
|
||||
Handler: chiRouter,
|
||||
},
|
||||
traffics: make(map[string]*TrafficManager),
|
||||
users: make(map[string]*UserManager),
|
||||
cachePath: options.CachePath,
|
||||
}
|
||||
inboundManager := service.FromContext[adapter.InboundManager](ctx)
|
||||
if options.Servers.Size() == 0 {
|
||||
@ -68,6 +74,8 @@ func NewService(ctx context.Context, logger log.ContextLogger, tag string, optio
|
||||
managedServer.SetTracker(traffic)
|
||||
user := NewUserManager(managedServer, traffic)
|
||||
chiRouter.Route(entry.Key, NewAPIServer(logger, traffic, user).Route)
|
||||
s.traffics[entry.Key] = traffic
|
||||
s.users[entry.Key] = user
|
||||
}
|
||||
if options.TLS != nil {
|
||||
tlsConfig, err := tls.NewServer(ctx, logger, common.PtrValueOrDefault(options.TLS))
|
||||
@ -83,8 +91,12 @@ func (s *Service) Start(stage adapter.StartStage) error {
|
||||
if stage != adapter.StartStateStart {
|
||||
return nil
|
||||
}
|
||||
err := s.loadCache()
|
||||
if err != nil {
|
||||
s.logger.Error(E.Cause(err, "load cache"))
|
||||
}
|
||||
if s.tlsConfig != nil {
|
||||
err := s.tlsConfig.Start()
|
||||
err = s.tlsConfig.Start()
|
||||
if err != nil {
|
||||
return E.Cause(err, "create TLS config")
|
||||
}
|
||||
@ -109,6 +121,10 @@ func (s *Service) Start(stage adapter.StartStage) error {
|
||||
}
|
||||
|
||||
func (s *Service) Close() error {
|
||||
err := s.saveCache()
|
||||
if err != nil {
|
||||
s.logger.Error(E.Cause(err, "save cache"))
|
||||
}
|
||||
return common.Close(
|
||||
common.PtrOrNil(s.httpServer),
|
||||
common.PtrOrNil(s.listener),
|
||||
|
@ -22,7 +22,7 @@ func NewUserManager(inbound adapter.ManagedSSMServer, trafficManager *TrafficMan
|
||||
}
|
||||
}
|
||||
|
||||
func (m *UserManager) postUpdate() error {
|
||||
func (m *UserManager) postUpdate(updated bool) error {
|
||||
users := make([]string, 0, len(m.usersMap))
|
||||
uPSKs := make([]string, 0, len(m.usersMap))
|
||||
for username, password := range m.usersMap {
|
||||
@ -33,7 +33,9 @@ func (m *UserManager) postUpdate() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.trafficManager.UpdateUsers(users)
|
||||
if updated {
|
||||
m.trafficManager.UpdateUsers(users)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -58,7 +60,7 @@ func (m *UserManager) Add(username string, password string) error {
|
||||
return E.New("user ", username, " already exists")
|
||||
}
|
||||
m.usersMap[username] = password
|
||||
return m.postUpdate()
|
||||
return m.postUpdate(true)
|
||||
}
|
||||
|
||||
func (m *UserManager) Get(username string) (string, bool) {
|
||||
@ -74,12 +76,12 @@ func (m *UserManager) Update(username string, password string) error {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
m.usersMap[username] = password
|
||||
return m.postUpdate()
|
||||
return m.postUpdate(true)
|
||||
}
|
||||
|
||||
func (m *UserManager) Delete(username string) error {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
delete(m.usersMap, username)
|
||||
return m.postUpdate()
|
||||
return m.postUpdate(true)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user