Compare commits

...

11 Commits

Author SHA1 Message Date
世界
a4c600d94a
documentation: Bump version 2025-08-15 15:06:47 +08:00
xchacha20-poly1305
900a2c1ef1
Fix rule set version 2025-08-15 15:06:47 +08:00
世界
4b3861232b
documentation: Add preferred_by route rule item 2025-08-15 15:06:47 +08:00
世界
31a0bc94ae
Add preferred_by route rule item 2025-08-15 15:06:47 +08:00
世界
8c1d1e9b0e
documentation: Add interface address rule items 2025-08-15 15:05:53 +08:00
世界
735b52fee5
Add interface address rule items 2025-08-15 15:05:52 +08:00
neletor
9fc9dccf46
Add support for ech retry configs 2025-08-15 15:05:52 +08:00
Zephyruso
c64f7fd8a1
Add /dns/flush-clash meta api 2025-08-15 15:05:52 +08:00
世界
de10bb00a9
Fix ssm-api 2025-08-15 15:05:37 +08:00
世界
fdc181106d
Fix atomic pointer usages 2025-08-15 15:05:34 +08:00
renovate[bot]
8752b631bd
[dependencies] Update golang Docker tag to v1.25 (#3276)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-15 12:45:43 +08:00
54 changed files with 1269 additions and 367 deletions

View File

@ -1,4 +1,4 @@
FROM --platform=$BUILDPLATFORM golang:1.24-alpine AS builder FROM --platform=$BUILDPLATFORM golang:1.25-alpine AS builder
LABEL maintainer="nekohasekai <contact-git@sekai.icu>" LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
COPY . /go/src/github.com/sagernet/sing-box COPY . /go/src/github.com/sagernet/sing-box
WORKDIR /go/src/github.com/sagernet/sing-box WORKDIR /go/src/github.com/sagernet/sing-box

View File

@ -2,6 +2,7 @@ package adapter
import ( import (
"context" "context"
"net/netip"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
@ -18,6 +19,11 @@ type Outbound interface {
N.Dialer N.Dialer
} }
type OutboundWithPreferredRoutes interface {
PreferredDomain(domain string) bool
PreferredAddress(address netip.Addr) bool
}
type OutboundRegistry interface { type OutboundRegistry interface {
option.OutboundOptionsRegistry option.OutboundOptionsRegistry
CreateOutbound(ctx context.Context, router Router, logger log.ContextLogger, tag string, outboundType string, options any) (Outbound, error) CreateOutbound(ctx context.Context, router Router, logger log.ContextLogger, tag string, outboundType string, options any) (Outbound, error)

View File

@ -6,8 +6,10 @@ import (
"strings" "strings"
"github.com/sagernet/sing-box/common/srs" "github.com/sagernet/sing-box/common/srs"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/route/rule"
"github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/json"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -69,7 +71,7 @@ func compileRuleSet(sourcePath string) error {
if err != nil { if err != nil {
return err return err
} }
err = srs.Write(outputFile, plainRuleSet.Options, plainRuleSet.Version) err = srs.Write(outputFile, plainRuleSet.Options, downgradeRuleSetVersion(plainRuleSet.Version, plainRuleSet.Options))
if err != nil { if err != nil {
outputFile.Close() outputFile.Close()
os.Remove(outputPath) os.Remove(outputPath)
@ -78,3 +80,18 @@ func compileRuleSet(sourcePath string) error {
outputFile.Close() outputFile.Close()
return nil return nil
} }
func downgradeRuleSetVersion(version uint8, options option.PlainRuleSet) uint8 {
if version == C.RuleSetVersion4 && !rule.HasHeadlessRule(options.Rules, func(rule option.DefaultHeadlessRule) bool {
return rule.NetworkInterfaceAddress != nil && rule.NetworkInterfaceAddress.Size() > 0 ||
len(rule.DefaultInterfaceAddress) > 0
}) {
version = C.RuleSetVersion3
}
if version == C.RuleSetVersion3 && !rule.HasHeadlessRule(options.Rules, func(rule option.DefaultHeadlessRule) bool {
return len(rule.NetworkType) > 0 || rule.NetworkIsExpensive || rule.NetworkIsConstrained
}) {
version = C.RuleSetVersion2
}
return version
}

View File

@ -10,6 +10,8 @@ import (
"sync" "sync"
"time" "time"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/atomic"
"github.com/sagernet/sing/common/bufio" "github.com/sagernet/sing/common/bufio"
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"
@ -22,7 +24,7 @@ type slowOpenConn struct {
ctx context.Context ctx context.Context
network string network string
destination M.Socksaddr destination M.Socksaddr
conn net.Conn conn atomic.Pointer[net.TCPConn]
create chan struct{} create chan struct{}
done chan struct{} done chan struct{}
access sync.Mutex access sync.Mutex
@ -50,22 +52,25 @@ func DialSlowContext(dialer *tcpDialer, ctx context.Context, network string, des
} }
func (c *slowOpenConn) Read(b []byte) (n int, err error) { func (c *slowOpenConn) Read(b []byte) (n int, err error) {
if c.conn == nil { conn := c.conn.Load()
select { if conn != nil {
case <-c.create: return conn.Read(b)
if c.err != nil { }
return 0, c.err select {
} case <-c.create:
case <-c.done: if c.err != nil {
return 0, os.ErrClosed return 0, c.err
} }
return c.conn.Load().Read(b)
case <-c.done:
return 0, os.ErrClosed
} }
return c.conn.Read(b)
} }
func (c *slowOpenConn) Write(b []byte) (n int, err error) { func (c *slowOpenConn) Write(b []byte) (n int, err error) {
if c.conn != nil { tcpConn := c.conn.Load()
return c.conn.Write(b) if tcpConn != nil {
return tcpConn.Write(b)
} }
c.access.Lock() c.access.Lock()
defer c.access.Unlock() defer c.access.Unlock()
@ -74,7 +79,7 @@ func (c *slowOpenConn) Write(b []byte) (n int, err error) {
if c.err != nil { if c.err != nil {
return 0, c.err return 0, c.err
} }
return c.conn.Write(b) return c.conn.Load().Write(b)
case <-c.done: case <-c.done:
return 0, os.ErrClosed return 0, os.ErrClosed
default: default:
@ -83,7 +88,7 @@ func (c *slowOpenConn) Write(b []byte) (n int, err error) {
if err != nil { if err != nil {
c.err = err c.err = err
} else { } else {
c.conn = conn c.conn.Store(conn.(*net.TCPConn))
} }
n = len(b) n = len(b)
close(c.create) close(c.create)
@ -93,70 +98,77 @@ func (c *slowOpenConn) Write(b []byte) (n int, err error) {
func (c *slowOpenConn) Close() error { func (c *slowOpenConn) Close() error {
c.closeOnce.Do(func() { c.closeOnce.Do(func() {
close(c.done) close(c.done)
if c.conn != nil { conn := c.conn.Load()
c.conn.Close() if conn != nil {
conn.Close()
} }
}) })
return nil return nil
} }
func (c *slowOpenConn) LocalAddr() net.Addr { func (c *slowOpenConn) LocalAddr() net.Addr {
if c.conn == nil { conn := c.conn.Load()
if conn == nil {
return M.Socksaddr{} return M.Socksaddr{}
} }
return c.conn.LocalAddr() return conn.LocalAddr()
} }
func (c *slowOpenConn) RemoteAddr() net.Addr { func (c *slowOpenConn) RemoteAddr() net.Addr {
if c.conn == nil { conn := c.conn.Load()
if conn == nil {
return M.Socksaddr{} return M.Socksaddr{}
} }
return c.conn.RemoteAddr() return conn.RemoteAddr()
} }
func (c *slowOpenConn) SetDeadline(t time.Time) error { func (c *slowOpenConn) SetDeadline(t time.Time) error {
if c.conn == nil { conn := c.conn.Load()
if conn == nil {
return os.ErrInvalid return os.ErrInvalid
} }
return c.conn.SetDeadline(t) return conn.SetDeadline(t)
} }
func (c *slowOpenConn) SetReadDeadline(t time.Time) error { func (c *slowOpenConn) SetReadDeadline(t time.Time) error {
if c.conn == nil { conn := c.conn.Load()
if conn == nil {
return os.ErrInvalid return os.ErrInvalid
} }
return c.conn.SetReadDeadline(t) return conn.SetReadDeadline(t)
} }
func (c *slowOpenConn) SetWriteDeadline(t time.Time) error { func (c *slowOpenConn) SetWriteDeadline(t time.Time) error {
if c.conn == nil { conn := c.conn.Load()
if conn == nil {
return os.ErrInvalid return os.ErrInvalid
} }
return c.conn.SetWriteDeadline(t) return conn.SetWriteDeadline(t)
} }
func (c *slowOpenConn) Upstream() any { func (c *slowOpenConn) Upstream() any {
return c.conn return common.PtrOrNil(c.conn.Load())
} }
func (c *slowOpenConn) ReaderReplaceable() bool { func (c *slowOpenConn) ReaderReplaceable() bool {
return c.conn != nil return c.conn.Load() != nil
} }
func (c *slowOpenConn) WriterReplaceable() bool { func (c *slowOpenConn) WriterReplaceable() bool {
return c.conn != nil return c.conn.Load() != nil
} }
func (c *slowOpenConn) LazyHeadroom() bool { func (c *slowOpenConn) LazyHeadroom() bool {
return c.conn == nil return c.conn.Load() == nil
} }
func (c *slowOpenConn) NeedHandshake() bool { func (c *slowOpenConn) NeedHandshake() bool {
return c.conn == nil return c.conn.Load() == nil
} }
func (c *slowOpenConn) WriteTo(w io.Writer) (n int64, err error) { func (c *slowOpenConn) WriteTo(w io.Writer) (n int64, err error) {
if c.conn == nil { conn := c.conn.Load()
if conn == nil {
select { select {
case <-c.create: case <-c.create:
if c.err != nil { if c.err != nil {
@ -166,5 +178,5 @@ func (c *slowOpenConn) WriteTo(w io.Writer) (n int64, err error) {
return 0, c.err return 0, c.err
} }
} }
return bufio.Copy(w, c.conn) return bufio.Copy(w, c.conn.Load())
} }

View File

@ -12,6 +12,8 @@ import (
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/domain" "github.com/sagernet/sing/common/domain"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json/badjson"
"github.com/sagernet/sing/common/json/badoption"
"github.com/sagernet/sing/common/varbin" "github.com/sagernet/sing/common/varbin"
"go4.org/netipx" "go4.org/netipx"
@ -41,6 +43,8 @@ const (
ruleItemNetworkType ruleItemNetworkType
ruleItemNetworkIsExpensive ruleItemNetworkIsExpensive
ruleItemNetworkIsConstrained ruleItemNetworkIsConstrained
ruleItemNetworkInterfaceAddress
ruleItemDefaultInterfaceAddress
ruleItemFinal uint8 = 0xFF ruleItemFinal uint8 = 0xFF
) )
@ -230,6 +234,51 @@ func readDefaultRule(reader varbin.Reader, recover bool) (rule option.DefaultHea
rule.NetworkIsExpensive = true rule.NetworkIsExpensive = true
case ruleItemNetworkIsConstrained: case ruleItemNetworkIsConstrained:
rule.NetworkIsConstrained = true rule.NetworkIsConstrained = true
case ruleItemNetworkInterfaceAddress:
rule.NetworkInterfaceAddress = new(badjson.TypedMap[option.InterfaceType, badoption.Listable[badoption.Prefixable]])
var size uint64
size, err = binary.ReadUvarint(reader)
if err != nil {
return
}
for i := uint64(0); i < size; i++ {
var key uint8
err = binary.Read(reader, binary.BigEndian, &key)
if err != nil {
return
}
var value []badoption.Prefixable
var prefixCount uint64
prefixCount, err = binary.ReadUvarint(reader)
if err != nil {
return
}
for j := uint64(0); j < prefixCount; j++ {
var prefix netip.Prefix
prefix, err = readPrefix(reader)
if err != nil {
return
}
value = append(value, badoption.Prefixable(prefix))
}
rule.NetworkInterfaceAddress.Put(option.InterfaceType(key), value)
}
case ruleItemDefaultInterfaceAddress:
var value []badoption.Prefixable
var prefixCount uint64
prefixCount, err = binary.ReadUvarint(reader)
if err != nil {
return
}
for j := uint64(0); j < prefixCount; j++ {
var prefix netip.Prefix
prefix, err = readPrefix(reader)
if err != nil {
return
}
value = append(value, badoption.Prefixable(prefix))
}
rule.DefaultInterfaceAddress = value
case ruleItemFinal: case ruleItemFinal:
err = binary.Read(reader, binary.BigEndian, &rule.Invert) err = binary.Read(reader, binary.BigEndian, &rule.Invert)
return return
@ -346,7 +395,7 @@ func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule, gen
} }
if len(rule.NetworkType) > 0 { if len(rule.NetworkType) > 0 {
if generateVersion < C.RuleSetVersion3 { if generateVersion < C.RuleSetVersion3 {
return E.New("network_type rule item is only supported in version 3 or later") return E.New("`network_type` rule item is only supported in version 3 or later")
} }
err = writeRuleItemUint8(writer, ruleItemNetworkType, rule.NetworkType) err = writeRuleItemUint8(writer, ruleItemNetworkType, rule.NetworkType)
if err != nil { if err != nil {
@ -354,17 +403,67 @@ func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule, gen
} }
} }
if rule.NetworkIsExpensive { if rule.NetworkIsExpensive {
if generateVersion < C.RuleSetVersion3 {
return E.New("`network_is_expensive` rule item is only supported in version 3 or later")
}
err = binary.Write(writer, binary.BigEndian, ruleItemNetworkIsExpensive) err = binary.Write(writer, binary.BigEndian, ruleItemNetworkIsExpensive)
if err != nil { if err != nil {
return err return err
} }
} }
if rule.NetworkIsConstrained { if rule.NetworkIsConstrained {
if generateVersion < C.RuleSetVersion3 {
return E.New("`network_is_constrained` rule item is only supported in version 3 or later")
}
err = binary.Write(writer, binary.BigEndian, ruleItemNetworkIsConstrained) err = binary.Write(writer, binary.BigEndian, ruleItemNetworkIsConstrained)
if err != nil { if err != nil {
return err return err
} }
} }
if rule.NetworkInterfaceAddress != nil && rule.NetworkInterfaceAddress.Size() > 0 {
if generateVersion < C.RuleSetVersion4 {
return E.New("`network_interface_address` rule item is only supported in version 4 or later")
}
err = writer.WriteByte(ruleItemNetworkInterfaceAddress)
if err != nil {
return err
}
_, err = varbin.WriteUvarint(writer, uint64(rule.NetworkInterfaceAddress.Size()))
if err != nil {
return err
}
for _, entry := range rule.NetworkInterfaceAddress.Entries() {
err = binary.Write(writer, binary.BigEndian, uint8(entry.Key.Build()))
if err != nil {
return err
}
for _, rawPrefix := range entry.Value {
err = writePrefix(writer, rawPrefix.Build(netip.Prefix{}))
if err != nil {
return err
}
}
}
}
if len(rule.DefaultInterfaceAddress) > 0 {
if generateVersion < C.RuleSetVersion4 {
return E.New("`default_interface_address` rule item is only supported in version 4 or later")
}
err = writer.WriteByte(ruleItemDefaultInterfaceAddress)
if err != nil {
return err
}
_, err = varbin.WriteUvarint(writer, uint64(len(rule.DefaultInterfaceAddress)))
if err != nil {
return err
}
for _, rawPrefix := range rule.DefaultInterfaceAddress {
err = writePrefix(writer, rawPrefix.Build(netip.Prefix{}))
if err != nil {
return err
}
}
}
if len(rule.WIFISSID) > 0 { if len(rule.WIFISSID) > 0 {
err = writeRuleItemString(writer, ruleItemWIFISSID, rule.WIFISSID) err = writeRuleItemString(writer, ruleItemWIFISSID, rule.WIFISSID)
if err != nil { if err != nil {

33
common/srs/ip_cidr.go Normal file
View File

@ -0,0 +1,33 @@
package srs
import (
"encoding/binary"
"net/netip"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/common/varbin"
)
func readPrefix(reader varbin.Reader) (netip.Prefix, error) {
addrSlice, err := varbin.ReadValue[[]byte](reader, binary.BigEndian)
if err != nil {
return netip.Prefix{}, err
}
prefixBits, err := varbin.ReadValue[uint8](reader, binary.BigEndian)
if err != nil {
return netip.Prefix{}, err
}
return netip.PrefixFrom(M.AddrFromIP(addrSlice), int(prefixBits)), nil
}
func writePrefix(writer varbin.Writer, prefix netip.Prefix) error {
err := varbin.Write(writer, binary.BigEndian, prefix.Addr().AsSlice())
if err != nil {
return err
}
err = binary.Write(writer, binary.BigEndian, uint8(prefix.Bits()))
if err != nil {
return err
}
return nil
}

View File

@ -2,10 +2,11 @@ package tls
import ( import (
"context" "context"
"crypto/tls"
"errors"
"net" "net"
"os" "os"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/badtls" "github.com/sagernet/sing-box/common/badtls"
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"
@ -14,7 +15,7 @@ import (
aTLS "github.com/sagernet/sing/common/tls" aTLS "github.com/sagernet/sing/common/tls"
) )
func NewDialerFromOptions(ctx context.Context, router adapter.Router, dialer N.Dialer, serverAddress string, options option.OutboundTLSOptions) (N.Dialer, error) { func NewDialerFromOptions(ctx context.Context, dialer N.Dialer, serverAddress string, options option.OutboundTLSOptions) (N.Dialer, error) {
if !options.Enabled { if !options.Enabled {
return dialer, nil return dialer, nil
} }
@ -53,26 +54,57 @@ func ClientHandshake(ctx context.Context, conn net.Conn, config Config) (Conn, e
return tlsConn, nil return tlsConn, nil
} }
type Dialer struct { type Dialer interface {
N.Dialer
DialTLSContext(ctx context.Context, destination M.Socksaddr) (Conn, error)
}
type defaultDialer struct {
dialer N.Dialer dialer N.Dialer
config Config config Config
} }
func NewDialer(dialer N.Dialer, config Config) N.Dialer { func NewDialer(dialer N.Dialer, config Config) Dialer {
return &Dialer{dialer, config} return &defaultDialer{dialer, config}
} }
func (d *Dialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { func (d *defaultDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
if network != N.NetworkTCP { if N.NetworkName(network) != N.NetworkTCP {
return nil, os.ErrInvalid return nil, os.ErrInvalid
} }
conn, err := d.dialer.DialContext(ctx, network, destination) return d.DialTLSContext(ctx, destination)
}
func (d *defaultDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
return nil, os.ErrInvalid
}
func (d *defaultDialer) DialTLSContext(ctx context.Context, destination M.Socksaddr) (Conn, error) {
return d.dialContext(ctx, destination, true)
}
func (d *defaultDialer) dialContext(ctx context.Context, destination M.Socksaddr, echRetry bool) (Conn, error) {
conn, err := d.dialer.DialContext(ctx, N.NetworkTCP, destination)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return ClientHandshake(ctx, conn, d.config) tlsConn, err := ClientHandshake(ctx, conn, d.config)
if err == nil {
return tlsConn, nil
}
conn.Close()
if echRetry {
var echErr *tls.ECHRejectionError
if errors.As(err, &echErr) && len(echErr.RetryConfigList) > 0 {
if echConfig, isECH := d.config.(ECHCapableConfig); isECH {
echConfig.SetECHConfigList(echErr.RetryConfigList)
}
}
return d.dialContext(ctx, destination, false)
}
return nil, err
} }
func (d *Dialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { func (d *defaultDialer) Upstream() any {
return nil, os.ErrInvalid return d.dialer
} }

View File

@ -22,7 +22,8 @@ const (
RuleSetVersion1 = 1 + iota RuleSetVersion1 = 1 + iota
RuleSetVersion2 RuleSetVersion2
RuleSetVersion3 RuleSetVersion3
RuleSetVersionCurrent = RuleSetVersion3 RuleSetVersion4
RuleSetVersionCurrent = RuleSetVersion4
) )
const ( const (

View File

@ -30,7 +30,7 @@ func RegisterTLS(registry *dns.TransportRegistry) {
type TLSTransport struct { type TLSTransport struct {
dns.TransportAdapter dns.TransportAdapter
logger logger.ContextLogger logger logger.ContextLogger
dialer N.Dialer dialer tls.Dialer
serverAddr M.Socksaddr serverAddr M.Socksaddr
tlsConfig tls.Config tlsConfig tls.Config
access sync.Mutex access sync.Mutex
@ -67,7 +67,7 @@ func NewTLSRaw(logger logger.ContextLogger, adapter dns.TransportAdapter, dialer
return &TLSTransport{ return &TLSTransport{
TransportAdapter: adapter, TransportAdapter: adapter,
logger: logger, logger: logger,
dialer: dialer, dialer: tls.NewDialer(dialer, tlsConfig),
serverAddr: serverAddr, serverAddr: serverAddr,
tlsConfig: tlsConfig, tlsConfig: tlsConfig,
} }
@ -100,15 +100,10 @@ func (t *TLSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.M
return response, nil return response, nil
} }
} }
tcpConn, err := t.dialer.DialContext(ctx, N.NetworkTCP, t.serverAddr) tlsConn, err := t.dialer.DialTLSContext(ctx, t.serverAddr)
if err != nil { if err != nil {
return nil, err return nil, err
} }
tlsConn, err := tls.ClientHandshake(ctx, tcpConn, t.tlsConfig)
if err != nil {
tcpConn.Close()
return nil, err
}
return t.exchange(message, &tlsDNSConn{Conn: tlsConn}) return t.exchange(message, &tlsDNSConn{Conn: tlsConn})
} }

View File

@ -2,11 +2,35 @@
icon: material/alert-decagram icon: material/alert-decagram
--- ---
#### 1.13.0-alpha.2
* Add `preferred_by` rule item **1**
* Fixes and improvements
**1**:
The new `preferred_by` routing rule item allows you to
match preferred domains and addresses for specific outbounds.
See [Route Rule](/configuration/route/rule/#preferred_by).
#### 1.13.0-alpha.1
* Add interface address rule items **1**
* Fixes and improvements
**1**:
New interface address rules allow you to dynamically adjust rules based on your network environment.
See [Route Rule](/configuration/route/rule/), [DNS Route Rule](/configuration/dns/rule/)
and [Headless Rule](/configuration/rule-set/headless-rule/).
#### 1.12.1 #### 1.12.1
* Fixes and improvements * Fixes and improvements
#### 1.12.0 ### 1.12.0
* Refactor DNS servers **1** * Refactor DNS servers **1**
* Add domain resolver options**2** * Add domain resolver options**2**
@ -157,7 +181,7 @@ We continue to experience issues updating our sing-box apps on the App Store and
Until we rewrite and resubmit the apps, they are considered irrecoverable. Until we rewrite and resubmit the apps, they are considered irrecoverable.
Therefore, after this release, we will not be repeating this notice unless there is new information. Therefore, after this release, we will not be repeating this notice unless there is new information.
### 1.11.15 #### 1.11.15
* Fixes and improvements * Fixes and improvements
@ -173,7 +197,7 @@ violated the rules (TestFlight users are not affected)._
We have significantly improved the performance of tun inbound on Apple platforms, especially in the gVisor stack. We have significantly improved the performance of tun inbound on Apple platforms, especially in the gVisor stack.
### 1.11.14 #### 1.11.14
* Fixes and improvements * Fixes and improvements
@ -223,7 +247,7 @@ You can now choose what the DERP home page shows, just like with derper's `-home
See [DERP](/configuration/service/derp/#home). See [DERP](/configuration/service/derp/#home).
### 1.11.13 #### 1.11.13
* Fixes and improvements * Fixes and improvements
@ -261,7 +285,7 @@ SSM API service is a RESTful API server for managing Shadowsocks servers.
See [SSM API Service](/configuration/service/ssm-api/). See [SSM API Service](/configuration/service/ssm-api/).
### 1.11.11 #### 1.11.11
* Fixes and improvements * Fixes and improvements
@ -293,7 +317,7 @@ You can now set `bind_interface`, `routing_mark` and `reuse_addr` in Listen Fiel
See [Listen Fields](/configuration/shared/listen/). See [Listen Fields](/configuration/shared/listen/).
### 1.11.10 #### 1.11.10
* Undeprecate the `block` outbound **1** * Undeprecate the `block` outbound **1**
* Fixes and improvements * Fixes and improvements
@ -311,7 +335,7 @@ violated the rules (TestFlight users are not affected)._
* Update quic-go to v0.51.0 * Update quic-go to v0.51.0
* Fixes and improvements * Fixes and improvements
### 1.11.9 #### 1.11.9
* Fixes and improvements * Fixes and improvements
@ -322,7 +346,7 @@ violated the rules (TestFlight users are not affected)._
* Fixes and improvements * Fixes and improvements
### 1.11.8 #### 1.11.8
* Improve `auto_redirect` **1** * Improve `auto_redirect` **1**
* Fixes and improvements * Fixes and improvements
@ -339,7 +363,7 @@ violated the rules (TestFlight users are not affected)._
* Fixes and improvements * Fixes and improvements
### 1.11.7 #### 1.11.7
* Fixes and improvements * Fixes and improvements
@ -355,7 +379,7 @@ violated the rules (TestFlight users are not affected)._
Now `auto_redirect` fixes compatibility issues between tun and Docker bridge networks, Now `auto_redirect` fixes compatibility issues between tun and Docker bridge networks,
see [Tun](/configuration/inbound/tun/#auto_redirect). see [Tun](/configuration/inbound/tun/#auto_redirect).
### 1.11.6 #### 1.11.6
* Fixes and improvements * Fixes and improvements
@ -396,7 +420,7 @@ See [Protocol Sniff](/configuration/route/sniff/).
See [Dial Fields](/configuration/shared/dial/#domain_resolver). See [Dial Fields](/configuration/shared/dial/#domain_resolver).
### 1.11.5 #### 1.11.5
* Fixes and improvements * Fixes and improvements
@ -412,7 +436,7 @@ violated the rules (TestFlight users are not affected)._
See [DNS Rule Action](/configuration/dns/rule_action/#predefined). See [DNS Rule Action](/configuration/dns/rule_action/#predefined).
### 1.11.4 #### 1.11.4
* Fixes and improvements * Fixes and improvements
@ -468,7 +492,7 @@ Due to maintenance difficulties, sing-box 1.12.0 requires at least Go 1.23 to co
For Windows 7 users, legacy binaries now continue to compile with Go 1.23 and patches from [MetaCubeX/go](https://github.com/MetaCubeX/go). For Windows 7 users, legacy binaries now continue to compile with Go 1.23 and patches from [MetaCubeX/go](https://github.com/MetaCubeX/go).
### 1.11.3 #### 1.11.3
* Fixes and improvements * Fixes and improvements
@ -479,7 +503,7 @@ process._
* Fixes and improvements * Fixes and improvements
### 1.11.1 #### 1.11.1
* Fixes and improvements * Fixes and improvements
@ -658,7 +682,7 @@ See [Hysteria2](/configuration/outbound/hysteria2/).
When `up_mbps` and `down_mbps` are set, `ignore_client_bandwidth` instead denies clients from using BBR CC. When `up_mbps` and `down_mbps` are set, `ignore_client_bandwidth` instead denies clients from using BBR CC.
### 1.10.7 #### 1.10.7
* Fixes and improvements * Fixes and improvements
@ -753,7 +777,7 @@ and the old outbound will be removed in sing-box 1.13.0.
See [Endpoint](/configuration/endpoint/), [WireGuard Endpoint](/configuration/endpoint/wireguard/) See [Endpoint](/configuration/endpoint/), [WireGuard Endpoint](/configuration/endpoint/wireguard/)
and [Migrate WireGuard outbound fields to route options](/migration/#migrate-wireguard-outbound-to-endpoint). and [Migrate WireGuard outbound fields to route options](/migration/#migrate-wireguard-outbound-to-endpoint).
### 1.10.2 #### 1.10.2
* Add deprecated warnings * Add deprecated warnings
* Fix proxying websocket connections in HTTP/mixed inbounds * Fix proxying websocket connections in HTTP/mixed inbounds
@ -890,7 +914,7 @@ See [Rule Action](/configuration/route/rule_action/).
* Update quic-go to v0.48.0 * Update quic-go to v0.48.0
* Fixes and improvements * Fixes and improvements
### 1.10.1 #### 1.10.1
* Fixes and improvements * Fixes and improvements

View File

@ -2,6 +2,12 @@
icon: material/alert-decagram icon: material/alert-decagram
--- ---
!!! quote "Changes in sing-box 1.13.0"
:material-plus: [interface_address](#interface_address)
:material-plus: [network_interface_address](#network_interface_address)
:material-plus: [default_interface_address](#default_interface_address)
!!! quote "Changes in sing-box 1.12.0" !!! quote "Changes in sing-box 1.12.0"
:material-plus: [ip_accept_any](#ip_accept_any) :material-plus: [ip_accept_any](#ip_accept_any)
@ -130,6 +136,19 @@ icon: material/alert-decagram
], ],
"network_is_expensive": false, "network_is_expensive": false,
"network_is_constrained": false, "network_is_constrained": false,
"interface_address": {
"en0": [
"2000::/3"
]
},
"network_interface_address": {
"wifi": [
"2000::/3"
]
},
"default_interface_address": [
"2000::/3"
],
"wifi_ssid": [ "wifi_ssid": [
"My WIFI" "My WIFI"
], ],
@ -359,6 +378,36 @@ such as Cellular or a Personal Hotspot (on Apple platforms).
Match if network is in Low Data Mode. Match if network is in Low Data Mode.
#### interface_address
!!! question "Since sing-box 1.13.0"
!!! quote ""
Only supported on Linux, Windows, and macOS.
Match interface address.
#### network_interface_address
!!! question "Since sing-box 1.13.0"
!!! quote ""
Only supported in graphical clients on Android and Apple platforms.
Matches network interface (same values as `network_type`) address.
#### default_interface_address
!!! question "Since sing-box 1.13.0"
!!! quote ""
Only supported on Linux, Windows, and macOS.
Match default interface address.
#### wifi_ssid #### wifi_ssid
!!! quote "" !!! quote ""

View File

@ -2,6 +2,12 @@
icon: material/alert-decagram icon: material/alert-decagram
--- ---
!!! quote "sing-box 1.13.0 中的更改"
:material-plus: [interface_address](#interface_address)
:material-plus: [network_interface_address](#network_interface_address)
:material-plus: [default_interface_address](#default_interface_address)
!!! quote "sing-box 1.12.0 中的更改" !!! quote "sing-box 1.12.0 中的更改"
:material-plus: [ip_accept_any](#ip_accept_any) :material-plus: [ip_accept_any](#ip_accept_any)
@ -130,6 +136,19 @@ icon: material/alert-decagram
], ],
"network_is_expensive": false, "network_is_expensive": false,
"network_is_constrained": false, "network_is_constrained": false,
"interface_address": {
"en0": [
"2000::/3"
]
},
"network_interface_address": {
"wifi": [
"2000::/3"
]
},
"default_interface_address": [
"2000::/3"
],
"wifi_ssid": [ "wifi_ssid": [
"My WIFI" "My WIFI"
], ],
@ -358,6 +377,36 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
匹配如果网络在低数据模式下。 匹配如果网络在低数据模式下。
#### interface_address
!!! question "自 sing-box 1.13.0 起"
!!! quote ""
仅支持 Linux、Windows 和 macOS.
匹配接口地址。
#### network_interface_address
!!! question "自 sing-box 1.13.0 起"
!!! quote ""
仅在 Android 与 Apple 平台图形客户端中支持。
匹配网络接口(可用值同 `network_type`)地址。
#### default_interface_address
!!! question "自 sing-box 1.13.0 起"
!!! quote ""
仅支持 Linux、Windows 和 macOS.
匹配默认接口地址。
#### wifi_ssid #### wifi_ssid
!!! quote "" !!! quote ""

View File

@ -2,6 +2,13 @@
icon: material/new-box icon: material/new-box
--- ---
!!! quote "Changes in sing-box 1.13.0"
:material-plus: [interface_address](#interface_address)
:material-plus: [network_interface_address](#network_interface_address)
:material-plus: [default_interface_address](#default_interface_address)
:material-plus: [preferred_by](#preferred_by)
!!! quote "Changes in sing-box 1.11.0" !!! quote "Changes in sing-box 1.11.0"
:material-plus: [action](#action) :material-plus: [action](#action)
@ -128,12 +135,29 @@ icon: material/new-box
], ],
"network_is_expensive": false, "network_is_expensive": false,
"network_is_constrained": false, "network_is_constrained": false,
"interface_address": {
"en0": [
"2000::/3"
]
},
"network_interface_address": {
"wifi": [
"2000::/3"
]
},
"default_interface_address": [
"2000::/3"
],
"wifi_ssid": [ "wifi_ssid": [
"My WIFI" "My WIFI"
], ],
"wifi_bssid": [ "wifi_bssid": [
"00:00:00:00:00:00" "00:00:00:00:00:00"
], ],
"preferred_by": [
"tailscale",
"wireguard"
],
"rule_set": [ "rule_set": [
"geoip-cn", "geoip-cn",
"geosite-cn" "geosite-cn"
@ -363,6 +387,36 @@ such as Cellular or a Personal Hotspot (on Apple platforms).
Match if network is in Low Data Mode. Match if network is in Low Data Mode.
#### interface_address
!!! question "Since sing-box 1.13.0"
!!! quote ""
Only supported on Linux, Windows, and macOS.
Match interface address.
#### network_interface_address
!!! question "Since sing-box 1.13.0"
!!! quote ""
Only supported in graphical clients on Android and Apple platforms.
Matches network interface (same values as `network_type`) address.
#### default_interface_address
!!! question "Since sing-box 1.13.0"
!!! quote ""
Only supported on Linux, Windows, and macOS.
Match default interface address.
#### wifi_ssid #### wifi_ssid
!!! quote "" !!! quote ""
@ -379,6 +433,17 @@ Match WiFi SSID.
Match WiFi BSSID. Match WiFi BSSID.
#### preferred_by
!!! question "Since sing-box 1.13.0"
Match specified outbounds' preferred routes.
| Type | Match |
|-------------|-----------------------------------------------|
| `tailscale` | Match MagicDNS domains and peers' allowed IPs |
| `wireguard` | Match peers's allowed IPs |
#### rule_set #### rule_set
!!! question "Since sing-box 1.8.0" !!! question "Since sing-box 1.8.0"

View File

@ -2,6 +2,13 @@
icon: material/new-box icon: material/new-box
--- ---
!!! quote "sing-box 1.13.0 中的更改"
:material-plus: [interface_address](#interface_address)
:material-plus: [network_interface_address](#network_interface_address)
:material-plus: [default_interface_address](#default_interface_address)
:material-plus: [preferred_by](#preferred_by)
!!! quote "sing-box 1.11.0 中的更改" !!! quote "sing-box 1.11.0 中的更改"
:material-plus: [action](#action) :material-plus: [action](#action)
@ -125,12 +132,29 @@ icon: material/new-box
], ],
"network_is_expensive": false, "network_is_expensive": false,
"network_is_constrained": false, "network_is_constrained": false,
"interface_address": {
"en0": [
"2000::/3"
]
},
"network_interface_address": {
"wifi": [
"2000::/3"
]
},
"default_interface_address": [
"2000::/3"
],
"wifi_ssid": [ "wifi_ssid": [
"My WIFI" "My WIFI"
], ],
"wifi_bssid": [ "wifi_bssid": [
"00:00:00:00:00:00" "00:00:00:00:00:00"
], ],
"preferred_by": [
"tailscale",
"wireguard"
],
"rule_set": [ "rule_set": [
"geoip-cn", "geoip-cn",
"geosite-cn" "geosite-cn"
@ -337,7 +361,7 @@ icon: material/new-box
匹配网络类型。 匹配网络类型。
Available values: `wifi`, `cellular`, `ethernet` and `other`. 可用值: `wifi`, `cellular`, `ethernet` and `other`.
#### network_is_expensive #### network_is_expensive
@ -360,6 +384,36 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
匹配如果网络在低数据模式下。 匹配如果网络在低数据模式下。
#### interface_address
!!! question "自 sing-box 1.13.0 起"
!!! quote ""
仅支持 Linux、Windows 和 macOS.
匹配接口地址。
#### network_interface_address
!!! question "自 sing-box 1.13.0 起"
!!! quote ""
仅在 Android 与 Apple 平台图形客户端中支持。
匹配网络接口(可用值同 `network_type`)地址。
#### default_interface_address
!!! question "自 sing-box 1.13.0 起"
!!! quote ""
仅支持 Linux、Windows 和 macOS.
匹配默认接口地址。
#### wifi_ssid #### wifi_ssid
!!! quote "" !!! quote ""
@ -376,6 +430,17 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
匹配 WiFi BSSID。 匹配 WiFi BSSID。
#### preferred_by
!!! question "自 sing-box 1.13.0 起"
匹配制定出站的首选路由。
| 类型 | 匹配 |
|-------------|--------------------------------|
| `tailscale` | 匹配 MagicDNS 域名和对端的 allowed IPs |
| `wireguard` | 匹配对端的 allowed IPs |
#### rule_set #### rule_set
!!! question "自 sing-box 1.8.0 起" !!! question "自 sing-box 1.8.0 起"

View File

@ -2,6 +2,11 @@
icon: material/new-box icon: material/new-box
--- ---
!!! quote "Changes in sing-box 1.13.0"
:material-plus: [network_interface_address](#network_interface_address)
:material-plus: [default_interface_address](#default_interface_address)
!!! quote "Changes in sing-box 1.11.0" !!! quote "Changes in sing-box 1.11.0"
:material-plus: [network_type](#network_type) :material-plus: [network_type](#network_type)
@ -78,6 +83,14 @@ icon: material/new-box
], ],
"network_is_expensive": false, "network_is_expensive": false,
"network_is_constrained": false, "network_is_constrained": false,
"network_interface_address": {
"wifi": [
"2000::/3"
]
},
"default_interface_address": [
"2000::/3"
],
"wifi_ssid": [ "wifi_ssid": [
"My WIFI" "My WIFI"
], ],
@ -225,6 +238,26 @@ such as Cellular or a Personal Hotspot (on Apple platforms).
Match if network is in Low Data Mode. Match if network is in Low Data Mode.
#### network_interface_address
!!! question "Since sing-box 1.13.0"
!!! quote ""
Only supported in graphical clients on Android and Apple platforms.
Matches network interface (same values as `network_type`) address.
#### default_interface_address
!!! question "Since sing-box 1.13.0"
!!! quote ""
Only supported on Linux, Windows, and macOS.
Match default interface address.
#### wifi_ssid #### wifi_ssid
!!! quote "" !!! quote ""

View File

@ -2,6 +2,11 @@
icon: material/new-box icon: material/new-box
--- ---
!!! quote "sing-box 1.13.0 中的更改"
:material-plus: [network_interface_address](#network_interface_address)
:material-plus: [default_interface_address](#default_interface_address)
!!! quote "sing-box 1.11.0 中的更改" !!! quote "sing-box 1.11.0 中的更改"
:material-plus: [network_type](#network_type) :material-plus: [network_type](#network_type)
@ -78,6 +83,14 @@ icon: material/new-box
], ],
"network_is_expensive": false, "network_is_expensive": false,
"network_is_constrained": false, "network_is_constrained": false,
"network_interface_address": {
"wifi": [
"2000::/3"
]
},
"default_interface_address": [
"2000::/3"
],
"wifi_ssid": [ "wifi_ssid": [
"My WIFI" "My WIFI"
], ],
@ -221,6 +234,26 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
匹配如果网络在低数据模式下。 匹配如果网络在低数据模式下。
#### network_interface_address
!!! question "自 sing-box 1.13.0 起"
!!! quote ""
仅在 Android 与 Apple 平台图形客户端中支持。
匹配网络接口(可用值同 `network_type`)地址。
#### default_interface_address
!!! question "自 sing-box 1.13.0 起"
!!! quote ""
仅支持 Linux、Windows 和 macOS.
匹配默认接口地址。
#### wifi_ssid #### wifi_ssid
!!! quote "" !!! quote ""

View File

@ -2,6 +2,10 @@
icon: material/new-box icon: material/new-box
--- ---
!!! quote "Changes in sing-box 1.13.0"
:material-plus: version `4`
!!! quote "Changes in sing-box 1.11.0" !!! quote "Changes in sing-box 1.11.0"
:material-plus: version `3` :material-plus: version `3`
@ -36,6 +40,7 @@ Version of rule-set.
* 1: sing-box 1.8.0: Initial rule-set version. * 1: sing-box 1.8.0: Initial rule-set version.
* 2: sing-box 1.10.0: Optimized memory usages of `domain_suffix` rules in binary rule-sets. * 2: sing-box 1.10.0: Optimized memory usages of `domain_suffix` rules in binary rule-sets.
* 3: sing-box 1.11.0: Added `network_type`, `network_is_expensive` and `network_is_constrainted` rule items. * 3: sing-box 1.11.0: Added `network_type`, `network_is_expensive` and `network_is_constrainted` rule items.
* 4: sing-box 1.13.0: Added `network_interface_address` and `default_interface_address` rule items.
#### rules #### rules

View File

@ -2,6 +2,10 @@
icon: material/new-box icon: material/new-box
--- ---
!!! quote "sing-box 1.13.0 中的更改"
:material-plus: version `4`
!!! quote "sing-box 1.11.0 中的更改" !!! quote "sing-box 1.11.0 中的更改"
:material-plus: version `3` :material-plus: version `3`
@ -36,6 +40,7 @@ icon: material/new-box
* 1: sing-box 1.8.0: 初始规则集版本。 * 1: sing-box 1.8.0: 初始规则集版本。
* 2: sing-box 1.10.0: 优化了二进制规则集中 `domain_suffix` 规则的内存使用。 * 2: sing-box 1.10.0: 优化了二进制规则集中 `domain_suffix` 规则的内存使用。
* 3: sing-box 1.11.0: 添加了 `network_type``network_is_expensive``network_is_constrainted` 规则项。 * 3: sing-box 1.11.0: 添加了 `network_type``network_is_expensive``network_is_constrainted` 规则项。
* 4: sing-box 1.13.0: 添加了 `network_interface_address``default_interface_address` 规则项。
#### rules #### rules

View File

@ -14,6 +14,7 @@ import (
func cacheRouter(ctx context.Context) http.Handler { func cacheRouter(ctx context.Context) http.Handler {
r := chi.NewRouter() r := chi.NewRouter()
r.Post("/fakeip/flush", flushFakeip(ctx)) r.Post("/fakeip/flush", flushFakeip(ctx))
r.Post("/dns/flush", flushDNS(ctx))
return r return r
} }
@ -31,3 +32,13 @@ func flushFakeip(ctx context.Context) func(w http.ResponseWriter, r *http.Reques
render.NoContent(w, r) render.NoContent(w, r)
} }
} }
func flushDNS(ctx context.Context) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
dnsRouter := service.FromContext[adapter.DNSRouter](ctx)
if dnsRouter != nil {
dnsRouter.ClearCache()
}
render.NoContent(w, r)
}
}

View File

@ -115,7 +115,7 @@ func (s *StatsService) RoutedPacketConnection(ctx context.Context, conn N.Packet
writeCounter = append(writeCounter, s.loadOrCreateCounter("user>>>"+user+">>>traffic>>>downlink")) writeCounter = append(writeCounter, s.loadOrCreateCounter("user>>>"+user+">>>traffic>>>downlink"))
} }
s.access.Unlock() s.access.Unlock()
return bufio.NewInt64CounterPacketConn(conn, readCounter, writeCounter) return bufio.NewInt64CounterPacketConn(conn, readCounter, nil, writeCounter, nil)
} }
func (s *StatsService) GetStats(ctx context.Context, request *GetStatsRequest) (*GetStatsResponse, error) { func (s *StatsService) GetStats(ctx context.Context, request *GetStatsRequest) (*GetStatsResponse, error) {

4
go.mod
View File

@ -27,9 +27,9 @@ require (
github.com/sagernet/gomobile v0.1.8 github.com/sagernet/gomobile v0.1.8
github.com/sagernet/gvisor v0.0.0-20250325023245-7a9c0f5725fb github.com/sagernet/gvisor v0.0.0-20250325023245-7a9c0f5725fb
github.com/sagernet/quic-go v0.52.0-beta.1 github.com/sagernet/quic-go v0.52.0-beta.1
github.com/sagernet/sing v0.7.5 github.com/sagernet/sing v0.7.6-0.20250815070458-d33ece7a184f
github.com/sagernet/sing-mux v0.3.3 github.com/sagernet/sing-mux v0.3.3
github.com/sagernet/sing-quic v0.5.0-beta.3 github.com/sagernet/sing-quic v0.5.0
github.com/sagernet/sing-shadowsocks v0.2.8 github.com/sagernet/sing-shadowsocks v0.2.8
github.com/sagernet/sing-shadowsocks2 v0.2.1 github.com/sagernet/sing-shadowsocks2 v0.2.1
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11

8
go.sum
View File

@ -167,12 +167,12 @@ github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/l
github.com/sagernet/quic-go v0.52.0-beta.1 h1:hWkojLg64zjV+MJOvJU/kOeWndm3tiEfBLx5foisszs= github.com/sagernet/quic-go v0.52.0-beta.1 h1:hWkojLg64zjV+MJOvJU/kOeWndm3tiEfBLx5foisszs=
github.com/sagernet/quic-go v0.52.0-beta.1/go.mod h1:OV+V5kEBb8kJS7k29MzDu6oj9GyMc7HA07sE1tedxz4= github.com/sagernet/quic-go v0.52.0-beta.1/go.mod h1:OV+V5kEBb8kJS7k29MzDu6oj9GyMc7HA07sE1tedxz4=
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 v0.7.5 h1:gNMwZCLPqR+4e0g6dwi0sSsrvOmoMjpZgqxKsuJZatc= github.com/sagernet/sing v0.7.6-0.20250815070458-d33ece7a184f h1:HIBo8l+tsS3wLwuI1E56uRTQw46QytXSUpZTP3vwG/U=
github.com/sagernet/sing v0.7.5/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/sagernet/sing v0.7.6-0.20250815070458-d33ece7a184f/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing-mux v0.3.3 h1:YFgt9plMWzH994BMZLmyKL37PdIVaIilwP0Jg+EcLfw= github.com/sagernet/sing-mux v0.3.3 h1:YFgt9plMWzH994BMZLmyKL37PdIVaIilwP0Jg+EcLfw=
github.com/sagernet/sing-mux v0.3.3/go.mod h1:pht8iFY4c9Xltj7rhVd208npkNaeCxzyXCgulDPLUDA= github.com/sagernet/sing-mux v0.3.3/go.mod h1:pht8iFY4c9Xltj7rhVd208npkNaeCxzyXCgulDPLUDA=
github.com/sagernet/sing-quic v0.5.0-beta.3 h1:X/acRNsqQNfDlmwE7SorHfaZiny5e67hqIzM/592ric= github.com/sagernet/sing-quic v0.5.0 h1:jNLIyVk24lFPvu8A4x+ZNEnZdI+Tg1rp7eCJ6v0Csak=
github.com/sagernet/sing-quic v0.5.0-beta.3/go.mod h1:SAv/qdeDN+75msGG5U5ZIwG+3Ua50jVIKNrRSY8pkx0= github.com/sagernet/sing-quic v0.5.0/go.mod h1:SAv/qdeDN+75msGG5U5ZIwG+3Ua50jVIKNrRSY8pkx0=
github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE= github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE=
github.com/sagernet/sing-shadowsocks v0.2.8/go.mod h1:lo7TWEMDcN5/h5B8S0ew+r78ZODn6SwVaFhvB6H+PTI= github.com/sagernet/sing-shadowsocks v0.2.8/go.mod h1:lo7TWEMDcN5/h5B8S0ew+r78ZODn6SwVaFhvB6H+PTI=
github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnqqs2gQ2/Qioo= github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnqqs2gQ2/Qioo=

View File

@ -67,42 +67,46 @@ func (r Rule) IsValid() bool {
} }
type RawDefaultRule struct { type RawDefaultRule struct {
Inbound badoption.Listable[string] `json:"inbound,omitempty"` Inbound badoption.Listable[string] `json:"inbound,omitempty"`
IPVersion int `json:"ip_version,omitempty"` IPVersion int `json:"ip_version,omitempty"`
Network badoption.Listable[string] `json:"network,omitempty"` Network badoption.Listable[string] `json:"network,omitempty"`
AuthUser badoption.Listable[string] `json:"auth_user,omitempty"` AuthUser badoption.Listable[string] `json:"auth_user,omitempty"`
Protocol badoption.Listable[string] `json:"protocol,omitempty"` Protocol badoption.Listable[string] `json:"protocol,omitempty"`
Client badoption.Listable[string] `json:"client,omitempty"` Client badoption.Listable[string] `json:"client,omitempty"`
Domain badoption.Listable[string] `json:"domain,omitempty"` Domain badoption.Listable[string] `json:"domain,omitempty"`
DomainSuffix badoption.Listable[string] `json:"domain_suffix,omitempty"` DomainSuffix badoption.Listable[string] `json:"domain_suffix,omitempty"`
DomainKeyword badoption.Listable[string] `json:"domain_keyword,omitempty"` DomainKeyword badoption.Listable[string] `json:"domain_keyword,omitempty"`
DomainRegex badoption.Listable[string] `json:"domain_regex,omitempty"` DomainRegex badoption.Listable[string] `json:"domain_regex,omitempty"`
Geosite badoption.Listable[string] `json:"geosite,omitempty"` Geosite badoption.Listable[string] `json:"geosite,omitempty"`
SourceGeoIP badoption.Listable[string] `json:"source_geoip,omitempty"` SourceGeoIP badoption.Listable[string] `json:"source_geoip,omitempty"`
GeoIP badoption.Listable[string] `json:"geoip,omitempty"` GeoIP badoption.Listable[string] `json:"geoip,omitempty"`
SourceIPCIDR badoption.Listable[string] `json:"source_ip_cidr,omitempty"` SourceIPCIDR badoption.Listable[string] `json:"source_ip_cidr,omitempty"`
SourceIPIsPrivate bool `json:"source_ip_is_private,omitempty"` SourceIPIsPrivate bool `json:"source_ip_is_private,omitempty"`
IPCIDR badoption.Listable[string] `json:"ip_cidr,omitempty"` IPCIDR badoption.Listable[string] `json:"ip_cidr,omitempty"`
IPIsPrivate bool `json:"ip_is_private,omitempty"` IPIsPrivate bool `json:"ip_is_private,omitempty"`
SourcePort badoption.Listable[uint16] `json:"source_port,omitempty"` SourcePort badoption.Listable[uint16] `json:"source_port,omitempty"`
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"`
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"`
PackageName badoption.Listable[string] `json:"package_name,omitempty"` PackageName badoption.Listable[string] `json:"package_name,omitempty"`
User badoption.Listable[string] `json:"user,omitempty"` User badoption.Listable[string] `json:"user,omitempty"`
UserID badoption.Listable[int32] `json:"user_id,omitempty"` UserID badoption.Listable[int32] `json:"user_id,omitempty"`
ClashMode string `json:"clash_mode,omitempty"` ClashMode string `json:"clash_mode,omitempty"`
NetworkType badoption.Listable[InterfaceType] `json:"network_type,omitempty"` NetworkType badoption.Listable[InterfaceType] `json:"network_type,omitempty"`
NetworkIsExpensive bool `json:"network_is_expensive,omitempty"` NetworkIsExpensive bool `json:"network_is_expensive,omitempty"`
NetworkIsConstrained bool `json:"network_is_constrained,omitempty"` NetworkIsConstrained bool `json:"network_is_constrained,omitempty"`
WIFISSID badoption.Listable[string] `json:"wifi_ssid,omitempty"` WIFISSID badoption.Listable[string] `json:"wifi_ssid,omitempty"`
WIFIBSSID badoption.Listable[string] `json:"wifi_bssid,omitempty"` WIFIBSSID badoption.Listable[string] `json:"wifi_bssid,omitempty"`
RuleSet badoption.Listable[string] `json:"rule_set,omitempty"` InterfaceAddress *badjson.TypedMap[string, badoption.Listable[badoption.Prefixable]] `json:"interface_address,omitempty"`
RuleSetIPCIDRMatchSource bool `json:"rule_set_ip_cidr_match_source,omitempty"` NetworkInterfaceAddress *badjson.TypedMap[InterfaceType, badoption.Listable[badoption.Prefixable]] `json:"network_interface_address,omitempty"`
Invert bool `json:"invert,omitempty"` DefaultInterfaceAddress badoption.Listable[badoption.Prefixable] `json:"default_interface_address,omitempty"`
PreferredBy badoption.Listable[string] `json:"preferred_by,omitempty"`
RuleSet badoption.Listable[string] `json:"rule_set,omitempty"`
RuleSetIPCIDRMatchSource bool `json:"rule_set_ip_cidr_match_source,omitempty"`
Invert bool `json:"invert,omitempty"`
// Deprecated: renamed to rule_set_ip_cidr_match_source // Deprecated: renamed to rule_set_ip_cidr_match_source
Deprecated_RulesetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,omitempty"` Deprecated_RulesetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,omitempty"`

View File

@ -68,45 +68,48 @@ func (r DNSRule) IsValid() bool {
} }
type RawDefaultDNSRule struct { type RawDefaultDNSRule struct {
Inbound badoption.Listable[string] `json:"inbound,omitempty"` Inbound badoption.Listable[string] `json:"inbound,omitempty"`
IPVersion int `json:"ip_version,omitempty"` IPVersion int `json:"ip_version,omitempty"`
QueryType badoption.Listable[DNSQueryType] `json:"query_type,omitempty"` QueryType badoption.Listable[DNSQueryType] `json:"query_type,omitempty"`
Network badoption.Listable[string] `json:"network,omitempty"` Network badoption.Listable[string] `json:"network,omitempty"`
AuthUser badoption.Listable[string] `json:"auth_user,omitempty"` AuthUser badoption.Listable[string] `json:"auth_user,omitempty"`
Protocol badoption.Listable[string] `json:"protocol,omitempty"` Protocol badoption.Listable[string] `json:"protocol,omitempty"`
Domain badoption.Listable[string] `json:"domain,omitempty"` Domain badoption.Listable[string] `json:"domain,omitempty"`
DomainSuffix badoption.Listable[string] `json:"domain_suffix,omitempty"` DomainSuffix badoption.Listable[string] `json:"domain_suffix,omitempty"`
DomainKeyword badoption.Listable[string] `json:"domain_keyword,omitempty"` DomainKeyword badoption.Listable[string] `json:"domain_keyword,omitempty"`
DomainRegex badoption.Listable[string] `json:"domain_regex,omitempty"` DomainRegex badoption.Listable[string] `json:"domain_regex,omitempty"`
Geosite badoption.Listable[string] `json:"geosite,omitempty"` Geosite badoption.Listable[string] `json:"geosite,omitempty"`
SourceGeoIP badoption.Listable[string] `json:"source_geoip,omitempty"` SourceGeoIP badoption.Listable[string] `json:"source_geoip,omitempty"`
GeoIP badoption.Listable[string] `json:"geoip,omitempty"` GeoIP badoption.Listable[string] `json:"geoip,omitempty"`
IPCIDR badoption.Listable[string] `json:"ip_cidr,omitempty"` IPCIDR badoption.Listable[string] `json:"ip_cidr,omitempty"`
IPIsPrivate bool `json:"ip_is_private,omitempty"` IPIsPrivate bool `json:"ip_is_private,omitempty"`
IPAcceptAny bool `json:"ip_accept_any,omitempty"` IPAcceptAny bool `json:"ip_accept_any,omitempty"`
SourceIPCIDR badoption.Listable[string] `json:"source_ip_cidr,omitempty"` SourceIPCIDR badoption.Listable[string] `json:"source_ip_cidr,omitempty"`
SourceIPIsPrivate bool `json:"source_ip_is_private,omitempty"` SourceIPIsPrivate bool `json:"source_ip_is_private,omitempty"`
SourcePort badoption.Listable[uint16] `json:"source_port,omitempty"` SourcePort badoption.Listable[uint16] `json:"source_port,omitempty"`
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"`
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"`
PackageName badoption.Listable[string] `json:"package_name,omitempty"` PackageName badoption.Listable[string] `json:"package_name,omitempty"`
User badoption.Listable[string] `json:"user,omitempty"` User badoption.Listable[string] `json:"user,omitempty"`
UserID badoption.Listable[int32] `json:"user_id,omitempty"` UserID badoption.Listable[int32] `json:"user_id,omitempty"`
Outbound badoption.Listable[string] `json:"outbound,omitempty"` Outbound badoption.Listable[string] `json:"outbound,omitempty"`
ClashMode string `json:"clash_mode,omitempty"` ClashMode string `json:"clash_mode,omitempty"`
NetworkType badoption.Listable[InterfaceType] `json:"network_type,omitempty"` NetworkType badoption.Listable[InterfaceType] `json:"network_type,omitempty"`
NetworkIsExpensive bool `json:"network_is_expensive,omitempty"` NetworkIsExpensive bool `json:"network_is_expensive,omitempty"`
NetworkIsConstrained bool `json:"network_is_constrained,omitempty"` NetworkIsConstrained bool `json:"network_is_constrained,omitempty"`
WIFISSID badoption.Listable[string] `json:"wifi_ssid,omitempty"` WIFISSID badoption.Listable[string] `json:"wifi_ssid,omitempty"`
WIFIBSSID badoption.Listable[string] `json:"wifi_bssid,omitempty"` WIFIBSSID badoption.Listable[string] `json:"wifi_bssid,omitempty"`
RuleSet badoption.Listable[string] `json:"rule_set,omitempty"` InterfaceAddress *badjson.TypedMap[string, badoption.Listable[badoption.Prefixable]] `json:"interface_address,omitempty"`
RuleSetIPCIDRMatchSource bool `json:"rule_set_ip_cidr_match_source,omitempty"` NetworkInterfaceAddress *badjson.TypedMap[InterfaceType, badoption.Listable[badoption.Prefixable]] `json:"network_interface_address,omitempty"`
RuleSetIPCIDRAcceptEmpty bool `json:"rule_set_ip_cidr_accept_empty,omitempty"` DefaultInterfaceAddress badoption.Listable[badoption.Prefixable] `json:"default_interface_address,omitempty"`
Invert bool `json:"invert,omitempty"` RuleSet badoption.Listable[string] `json:"rule_set,omitempty"`
RuleSetIPCIDRMatchSource bool `json:"rule_set_ip_cidr_match_source,omitempty"`
RuleSetIPCIDRAcceptEmpty bool `json:"rule_set_ip_cidr_accept_empty,omitempty"`
Invert bool `json:"invert,omitempty"`
// Deprecated: renamed to rule_set_ip_cidr_match_source // Deprecated: renamed to rule_set_ip_cidr_match_source
Deprecated_RulesetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,omitempty"` Deprecated_RulesetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,omitempty"`

View File

@ -182,28 +182,31 @@ func (r HeadlessRule) IsValid() bool {
} }
type DefaultHeadlessRule struct { type DefaultHeadlessRule struct {
QueryType badoption.Listable[DNSQueryType] `json:"query_type,omitempty"` QueryType badoption.Listable[DNSQueryType] `json:"query_type,omitempty"`
Network badoption.Listable[string] `json:"network,omitempty"` Network badoption.Listable[string] `json:"network,omitempty"`
Domain badoption.Listable[string] `json:"domain,omitempty"` Domain badoption.Listable[string] `json:"domain,omitempty"`
DomainSuffix badoption.Listable[string] `json:"domain_suffix,omitempty"` DomainSuffix badoption.Listable[string] `json:"domain_suffix,omitempty"`
DomainKeyword badoption.Listable[string] `json:"domain_keyword,omitempty"` DomainKeyword badoption.Listable[string] `json:"domain_keyword,omitempty"`
DomainRegex badoption.Listable[string] `json:"domain_regex,omitempty"` DomainRegex badoption.Listable[string] `json:"domain_regex,omitempty"`
SourceIPCIDR badoption.Listable[string] `json:"source_ip_cidr,omitempty"` SourceIPCIDR badoption.Listable[string] `json:"source_ip_cidr,omitempty"`
IPCIDR badoption.Listable[string] `json:"ip_cidr,omitempty"` IPCIDR badoption.Listable[string] `json:"ip_cidr,omitempty"`
SourcePort badoption.Listable[uint16] `json:"source_port,omitempty"` SourcePort badoption.Listable[uint16] `json:"source_port,omitempty"`
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"`
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"`
PackageName badoption.Listable[string] `json:"package_name,omitempty"` PackageName badoption.Listable[string] `json:"package_name,omitempty"`
NetworkType badoption.Listable[InterfaceType] `json:"network_type,omitempty"` NetworkType badoption.Listable[InterfaceType] `json:"network_type,omitempty"`
NetworkIsExpensive bool `json:"network_is_expensive,omitempty"` NetworkIsExpensive bool `json:"network_is_expensive,omitempty"`
NetworkIsConstrained bool `json:"network_is_constrained,omitempty"` NetworkIsConstrained bool `json:"network_is_constrained,omitempty"`
WIFISSID badoption.Listable[string] `json:"wifi_ssid,omitempty"` WIFISSID badoption.Listable[string] `json:"wifi_ssid,omitempty"`
WIFIBSSID badoption.Listable[string] `json:"wifi_bssid,omitempty"` WIFIBSSID badoption.Listable[string] `json:"wifi_bssid,omitempty"`
Invert bool `json:"invert,omitempty"` NetworkInterfaceAddress *badjson.TypedMap[InterfaceType, badoption.Listable[badoption.Prefixable]] `json:"network_interface_address,omitempty"`
DefaultInterfaceAddress badoption.Listable[badoption.Prefixable] `json:"default_interface_address,omitempty"`
Invert bool `json:"invert,omitempty"`
DomainMatcher *domain.Matcher `json:"-"` DomainMatcher *domain.Matcher `json:"-"`
SourceIPSet *netipx.IPSet `json:"-"` SourceIPSet *netipx.IPSet `json:"-"`
@ -240,7 +243,7 @@ type PlainRuleSetCompat _PlainRuleSetCompat
func (r PlainRuleSetCompat) MarshalJSON() ([]byte, error) { func (r PlainRuleSetCompat) MarshalJSON() ([]byte, error) {
var v any var v any
switch r.Version { switch r.Version {
case C.RuleSetVersion1, C.RuleSetVersion2, C.RuleSetVersion3: case C.RuleSetVersion1, C.RuleSetVersion2, C.RuleSetVersion3, C.RuleSetVersion4:
v = r.Options v = r.Options
default: default:
return nil, E.New("unknown rule-set version: ", r.Version) return nil, E.New("unknown rule-set version: ", r.Version)
@ -255,7 +258,7 @@ func (r *PlainRuleSetCompat) UnmarshalJSON(bytes []byte) error {
} }
var v any var v any
switch r.Version { switch r.Version {
case C.RuleSetVersion1, C.RuleSetVersion2, C.RuleSetVersion3: case C.RuleSetVersion1, C.RuleSetVersion2, C.RuleSetVersion3, C.RuleSetVersion4:
v = &r.Options v = &r.Options
case 0: case 0:
return E.New("missing rule-set version") return E.New("missing rule-set version")
@ -272,7 +275,7 @@ func (r *PlainRuleSetCompat) UnmarshalJSON(bytes []byte) error {
func (r PlainRuleSetCompat) Upgrade() (PlainRuleSet, error) { func (r PlainRuleSetCompat) Upgrade() (PlainRuleSet, error) {
switch r.Version { switch r.Version {
case C.RuleSetVersion1, C.RuleSetVersion2, C.RuleSetVersion3: case C.RuleSetVersion1, C.RuleSetVersion2, C.RuleSetVersion3, C.RuleSetVersion4:
default: default:
return PlainRuleSet{}, E.New("unknown rule-set version: " + F.ToString(r.Version)) return PlainRuleSet{}, E.New("unknown rule-set version: " + F.ToString(r.Version))
} }

View File

@ -26,7 +26,7 @@ func RegisterOutbound(registry *outbound.Registry) {
type Outbound struct { type Outbound struct {
outbound.Adapter outbound.Adapter
dialer N.Dialer dialer tls.Dialer
server M.Socksaddr server M.Socksaddr
tlsConfig tls.Config tlsConfig tls.Config
client *anytls.Client client *anytls.Client
@ -58,7 +58,8 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
if err != nil { if err != nil {
return nil, err return nil, err
} }
outbound.dialer = outboundDialer
outbound.dialer = tls.NewDialer(outboundDialer, tlsConfig)
client, err := anytls.NewClient(ctx, anytls.ClientConfig{ client, err := anytls.NewClient(ctx, anytls.ClientConfig{
Password: options.Password, Password: options.Password,
@ -91,16 +92,7 @@ func (d anytlsDialer) ListenPacket(ctx context.Context, destination M.Socksaddr)
} }
func (h *Outbound) dialOut(ctx context.Context) (net.Conn, error) { func (h *Outbound) dialOut(ctx context.Context) (net.Conn, error) {
conn, err := h.dialer.DialContext(ctx, N.NetworkTCP, h.server) return h.dialer.DialTLSContext(ctx, h.server)
if err != nil {
return nil, err
}
tlsConn, err := tls.ClientHandshake(ctx, conn, h.tlsConfig)
if err != nil {
common.Close(tlsConn, conn)
return nil, err
}
return tlsConn, nil
} }
func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {

View File

@ -34,7 +34,7 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
if err != nil { if err != nil {
return nil, err return nil, err
} }
detour, err := tls.NewDialerFromOptions(ctx, router, outboundDialer, options.Server, common.PtrValueOrDefault(options.TLS)) detour, err := tls.NewDialerFromOptions(ctx, outboundDialer, options.Server, common.PtrValueOrDefault(options.TLS))
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -7,7 +7,6 @@ import (
"net/netip" "net/netip"
"net/url" "net/url"
"os" "os"
"reflect"
"strings" "strings"
"sync" "sync"
@ -47,8 +46,6 @@ type DNSTransport struct {
acceptDefaultResolvers bool acceptDefaultResolvers bool
dnsRouter adapter.DNSRouter dnsRouter adapter.DNSRouter
endpointManager adapter.EndpointManager endpointManager adapter.EndpointManager
cfg *wgcfg.Config
dnsCfg *nDNS.Config
endpoint *Endpoint endpoint *Endpoint
routePrefixes []netip.Prefix routePrefixes []netip.Prefix
routes map[string][]adapter.DNSTransport routes map[string][]adapter.DNSTransport
@ -83,10 +80,10 @@ func (t *DNSTransport) Start(stage adapter.StartStage) error {
if !isTailscale { if !isTailscale {
return E.New("endpoint is not Tailscale: ", t.endpointTag) return E.New("endpoint is not Tailscale: ", t.endpointTag)
} }
if ep.onReconfig != nil { if ep.onReconfigHook != nil {
return E.New("only one Tailscale DNS server is allowed for single endpoint") return E.New("only one Tailscale DNS server is allowed for single endpoint")
} }
ep.onReconfig = t.onReconfig ep.onReconfigHook = t.onReconfig
t.endpoint = ep t.endpoint = ep
return nil return nil
} }
@ -95,14 +92,6 @@ func (t *DNSTransport) Reset() {
} }
func (t *DNSTransport) onReconfig(cfg *wgcfg.Config, routerCfg *router.Config, dnsCfg *nDNS.Config) { func (t *DNSTransport) onReconfig(cfg *wgcfg.Config, routerCfg *router.Config, dnsCfg *nDNS.Config) {
if cfg == nil || dnsCfg == nil {
return
}
if (t.cfg != nil && reflect.DeepEqual(t.cfg, cfg)) && (t.dnsCfg != nil && reflect.DeepEqual(t.dnsCfg, dnsCfg)) {
return
}
t.cfg = cfg
t.dnsCfg = dnsCfg
err := t.updateDNSServers(routerCfg, dnsCfg) err := t.updateDNSServers(routerCfg, dnsCfg)
if err != nil { if err != nil {
t.logger.Error(E.Cause(err, "update DNS servers")) t.logger.Error(E.Cause(err, "update DNS servers"))

View File

@ -10,9 +10,9 @@ import (
"net/url" "net/url"
"os" "os"
"path/filepath" "path/filepath"
"reflect"
"runtime" "runtime"
"strings" "strings"
"sync/atomic"
"syscall" "syscall"
"time" "time"
@ -31,6 +31,7 @@ import (
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-tun" "github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/atomic"
"github.com/sagernet/sing/common/bufio" "github.com/sagernet/sing/common/bufio"
"github.com/sagernet/sing/common/control" "github.com/sagernet/sing/common/control"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
@ -49,8 +50,14 @@ import (
"github.com/sagernet/tailscale/version" "github.com/sagernet/tailscale/version"
"github.com/sagernet/tailscale/wgengine" "github.com/sagernet/tailscale/wgengine"
"github.com/sagernet/tailscale/wgengine/filter" "github.com/sagernet/tailscale/wgengine/filter"
"github.com/sagernet/tailscale/wgengine/router"
"github.com/sagernet/tailscale/wgengine/wgcfg"
"go4.org/netipx"
) )
var _ adapter.OutboundWithPreferredRoutes = (*Endpoint)(nil)
func init() { func init() {
version.SetVersion("sing-box " + C.Version) version.SetVersion("sing-box " + C.Version)
} }
@ -70,7 +77,12 @@ type Endpoint struct {
server *tsnet.Server server *tsnet.Server
stack *stack.Stack stack *stack.Stack
filter *atomic.Pointer[filter.Filter] filter *atomic.Pointer[filter.Filter]
onReconfig wgengine.ReconfigListener onReconfigHook wgengine.ReconfigListener
cfg *wgcfg.Config
dnsCfg *tsDNS.Config
routeDomains atomic.TypedValue[map[string]bool]
routePrefixes atomic.Pointer[netipx.IPSet]
acceptRoutes bool acceptRoutes bool
exitNode string exitNode string
@ -216,9 +228,7 @@ func (t *Endpoint) Start(stage adapter.StartStage) error {
if err != nil { if err != nil {
return err return err
} }
if t.onReconfig != nil { t.server.ExportLocalBackend().ExportEngine().(wgengine.ExportedUserspaceEngine).SetOnReconfigListener(t.onReconfig)
t.server.ExportLocalBackend().ExportEngine().(wgengine.ExportedUserspaceEngine).SetOnReconfigListener(t.onReconfig)
}
ipStack := t.server.ExportNetstack().ExportIPStack() ipStack := t.server.ExportNetstack().ExportIPStack()
gErr := ipStack.SetSpoofing(tun.DefaultNIC, true) gErr := ipStack.SetSpoofing(tun.DefaultNIC, true)
@ -253,8 +263,7 @@ func (t *Endpoint) Start(stage adapter.StartStage) error {
if err != nil { if err != nil {
return E.Cause(err, "update prefs") return E.Cause(err, "update prefs")
} }
t.filter = localBackend.ExportFilter() t.filter = atomic.PointerForm(localBackend.ExportFilter())
go t.watchState() go t.watchState()
return nil return nil
} }
@ -473,10 +482,58 @@ func (t *Endpoint) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn,
t.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) t.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose)
} }
func (t *Endpoint) PreferredDomain(domain string) bool {
routeDomains := t.routeDomains.Load()
if routeDomains == nil {
return false
}
return routeDomains[strings.ToLower(domain)]
}
func (t *Endpoint) PreferredAddress(address netip.Addr) bool {
routePrefixes := t.routePrefixes.Load()
if routePrefixes == nil {
return false
}
return routePrefixes.Contains(address)
}
func (t *Endpoint) Server() *tsnet.Server { func (t *Endpoint) Server() *tsnet.Server {
return t.server return t.server
} }
func (t *Endpoint) onReconfig(cfg *wgcfg.Config, routerCfg *router.Config, dnsCfg *tsDNS.Config) {
if cfg == nil || dnsCfg == nil {
return
}
if (t.cfg != nil && reflect.DeepEqual(t.cfg, cfg)) && (t.dnsCfg != nil && reflect.DeepEqual(t.dnsCfg, dnsCfg)) {
return
}
t.cfg = cfg
t.dnsCfg = dnsCfg
routeDomains := make(map[string]bool)
for fqdn := range dnsCfg.Routes {
routeDomains[fqdn.WithoutTrailingDot()] = true
}
for _, fqdn := range dnsCfg.SearchDomains {
routeDomains[fqdn.WithoutTrailingDot()] = true
}
t.routeDomains.Store(routeDomains)
var builder netipx.IPSetBuilder
for _, peer := range cfg.Peers {
for _, allowedIP := range peer.AllowedIPs {
builder.AddPrefix(allowedIP)
}
}
t.routePrefixes.Store(common.Must1(builder.IPSet()))
if t.onReconfigHook != nil {
t.onReconfigHook(cfg, routerCfg, dnsCfg)
}
}
func addressFromAddr(destination netip.Addr) tcpip.Address { func addressFromAddr(destination netip.Addr) tcpip.Address {
if destination.Is6() { if destination.Is6() {
return tcpip.AddrFrom16(destination.As16()) return tcpip.AddrFrom16(destination.As16())

View File

@ -34,6 +34,7 @@ type Outbound struct {
key [56]byte key [56]byte
multiplexDialer *mux.Client multiplexDialer *mux.Client
tlsConfig tls.Config tlsConfig tls.Config
tlsDialer tls.Dialer
transport adapter.V2RayClientTransport transport adapter.V2RayClientTransport
} }
@ -54,6 +55,7 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
if err != nil { if err != nil {
return nil, err return nil, err
} }
outbound.tlsDialer = tls.NewDialer(outboundDialer, outbound.tlsConfig)
} }
if options.Transport != nil { if options.Transport != nil {
outbound.transport, err = v2ray.NewClientTransport(ctx, outbound.dialer, outbound.serverAddr, common.PtrValueOrDefault(options.Transport), outbound.tlsConfig) outbound.transport, err = v2ray.NewClientTransport(ctx, outbound.dialer, outbound.serverAddr, common.PtrValueOrDefault(options.Transport), outbound.tlsConfig)
@ -121,11 +123,10 @@ func (h *trojanDialer) DialContext(ctx context.Context, network string, destinat
var err error var err error
if h.transport != nil { if h.transport != nil {
conn, err = h.transport.DialContext(ctx) conn, err = h.transport.DialContext(ctx)
} else if h.tlsDialer != nil {
conn, err = h.tlsDialer.DialTLSContext(ctx, h.serverAddr)
} else { } else {
conn, err = h.dialer.DialContext(ctx, N.NetworkTCP, h.serverAddr) conn, err = h.dialer.DialContext(ctx, N.NetworkTCP, h.serverAddr)
if err == nil && h.tlsConfig != nil {
conn, err = tls.ClientHandshake(ctx, conn, h.tlsConfig)
}
} }
if err != nil { if err != nil {
common.Close(conn) common.Close(conn)

View File

@ -35,6 +35,7 @@ type Outbound struct {
serverAddr M.Socksaddr serverAddr M.Socksaddr
multiplexDialer *mux.Client multiplexDialer *mux.Client
tlsConfig tls.Config tlsConfig tls.Config
tlsDialer tls.Dialer
transport adapter.V2RayClientTransport transport adapter.V2RayClientTransport
packetAddr bool packetAddr bool
xudp bool xudp bool
@ -56,6 +57,7 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
if err != nil { if err != nil {
return nil, err return nil, err
} }
outbound.tlsDialer = tls.NewDialer(outboundDialer, outbound.tlsConfig)
} }
if options.Transport != nil { if options.Transport != nil {
outbound.transport, err = v2ray.NewClientTransport(ctx, outbound.dialer, outbound.serverAddr, common.PtrValueOrDefault(options.Transport), outbound.tlsConfig) outbound.transport, err = v2ray.NewClientTransport(ctx, outbound.dialer, outbound.serverAddr, common.PtrValueOrDefault(options.Transport), outbound.tlsConfig)
@ -140,11 +142,10 @@ func (h *vlessDialer) DialContext(ctx context.Context, network string, destinati
var err error var err error
if h.transport != nil { if h.transport != nil {
conn, err = h.transport.DialContext(ctx) conn, err = h.transport.DialContext(ctx)
} else if h.tlsDialer != nil {
conn, err = h.tlsDialer.DialTLSContext(ctx, h.serverAddr)
} else { } else {
conn, err = h.dialer.DialContext(ctx, N.NetworkTCP, h.serverAddr) conn, err = h.dialer.DialContext(ctx, N.NetworkTCP, h.serverAddr)
if err == nil && h.tlsConfig != nil {
conn, err = tls.ClientHandshake(ctx, conn, h.tlsConfig)
}
} }
if err != nil { if err != nil {
return nil, err return nil, err
@ -183,11 +184,10 @@ func (h *vlessDialer) ListenPacket(ctx context.Context, destination M.Socksaddr)
var err error var err error
if h.transport != nil { if h.transport != nil {
conn, err = h.transport.DialContext(ctx) conn, err = h.transport.DialContext(ctx)
} else if h.tlsDialer != nil {
conn, err = h.tlsDialer.DialTLSContext(ctx, h.serverAddr)
} else { } else {
conn, err = h.dialer.DialContext(ctx, N.NetworkTCP, h.serverAddr) conn, err = h.dialer.DialContext(ctx, N.NetworkTCP, h.serverAddr)
if err == nil && h.tlsConfig != nil {
conn, err = tls.ClientHandshake(ctx, conn, h.tlsConfig)
}
} }
if err != nil { if err != nil {
common.Close(conn) common.Close(conn)

View File

@ -35,6 +35,7 @@ type Outbound struct {
serverAddr M.Socksaddr serverAddr M.Socksaddr
multiplexDialer *mux.Client multiplexDialer *mux.Client
tlsConfig tls.Config tlsConfig tls.Config
tlsDialer tls.Dialer
transport adapter.V2RayClientTransport transport adapter.V2RayClientTransport
packetAddr bool packetAddr bool
xudp bool xudp bool
@ -56,6 +57,7 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
if err != nil { if err != nil {
return nil, err return nil, err
} }
outbound.tlsDialer = tls.NewDialer(outboundDialer, outbound.tlsConfig)
} }
if options.Transport != nil { if options.Transport != nil {
outbound.transport, err = v2ray.NewClientTransport(ctx, outbound.dialer, outbound.serverAddr, common.PtrValueOrDefault(options.Transport), outbound.tlsConfig) outbound.transport, err = v2ray.NewClientTransport(ctx, outbound.dialer, outbound.serverAddr, common.PtrValueOrDefault(options.Transport), outbound.tlsConfig)
@ -154,11 +156,10 @@ func (h *vmessDialer) DialContext(ctx context.Context, network string, destinati
var err error var err error
if h.transport != nil { if h.transport != nil {
conn, err = h.transport.DialContext(ctx) conn, err = h.transport.DialContext(ctx)
} else if h.tlsDialer != nil {
conn, err = h.tlsDialer.DialTLSContext(ctx, h.serverAddr)
} else { } else {
conn, err = h.dialer.DialContext(ctx, N.NetworkTCP, h.serverAddr) conn, err = h.dialer.DialContext(ctx, N.NetworkTCP, h.serverAddr)
if err == nil && h.tlsConfig != nil {
conn, err = tls.ClientHandshake(ctx, conn, h.tlsConfig)
}
} }
if err != nil { if err != nil {
common.Close(conn) common.Close(conn)
@ -182,11 +183,10 @@ func (h *vmessDialer) ListenPacket(ctx context.Context, destination M.Socksaddr)
var err error var err error
if h.transport != nil { if h.transport != nil {
conn, err = h.transport.DialContext(ctx) conn, err = h.transport.DialContext(ctx)
} else if h.tlsDialer != nil {
conn, err = h.tlsDialer.DialTLSContext(ctx, h.serverAddr)
} else { } else {
conn, err = h.dialer.DialContext(ctx, N.NetworkTCP, h.serverAddr) conn, err = h.dialer.DialContext(ctx, N.NetworkTCP, h.serverAddr)
if err == nil && h.tlsConfig != nil {
conn, err = tls.ClientHandshake(ctx, conn, h.tlsConfig)
}
} }
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -22,6 +22,8 @@ import (
"github.com/sagernet/sing/service" "github.com/sagernet/sing/service"
) )
var _ adapter.OutboundWithPreferredRoutes = (*Endpoint)(nil)
func RegisterEndpoint(registry *endpoint.Registry) { func RegisterEndpoint(registry *endpoint.Registry) {
endpoint.Register[option.WireGuardEndpointOptions](registry, C.TypeWireGuard, NewEndpoint) endpoint.Register[option.WireGuardEndpointOptions](registry, C.TypeWireGuard, NewEndpoint)
} }
@ -210,3 +212,11 @@ func (w *Endpoint) ListenPacket(ctx context.Context, destination M.Socksaddr) (n
} }
return w.endpoint.ListenPacket(ctx, destination) return w.endpoint.ListenPacket(ctx, destination)
} }
func (w *Endpoint) PreferredDomain(domain string) bool {
return false
}
func (w *Endpoint) PreferredAddress(address netip.Addr) bool {
return w.endpoint.Lookup(address) != nil
}

View File

@ -21,6 +21,8 @@ import (
"github.com/sagernet/sing/service" "github.com/sagernet/sing/service"
) )
var _ adapter.OutboundWithPreferredRoutes = (*Outbound)(nil)
func RegisterOutbound(registry *outbound.Registry) { func RegisterOutbound(registry *outbound.Registry) {
outbound.Register[option.LegacyWireGuardOutboundOptions](registry, C.TypeWireGuard, NewOutbound) outbound.Register[option.LegacyWireGuardOutboundOptions](registry, C.TypeWireGuard, NewOutbound)
} }
@ -158,3 +160,11 @@ func (o *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (n
} }
return o.endpoint.ListenPacket(ctx, destination) return o.endpoint.ListenPacket(ctx, destination)
} }
func (o *Outbound) PreferredDomain(domain string) bool {
return false
}
func (o *Outbound) PreferredAddress(address netip.Addr) bool {
return o.endpoint.Lookup(address) != nil
}

View File

@ -117,7 +117,7 @@ func NewDefaultRule(ctx context.Context, logger log.ContextLogger, options optio
if len(options.DomainRegex) > 0 { if len(options.DomainRegex) > 0 {
item, err := NewDomainRegexItem(options.DomainRegex) item, err := NewDomainRegexItem(options.DomainRegex)
if err != nil { if err != nil {
return nil, E.Cause(err, "domain_regex") return nil, err
} }
rule.destinationAddressItems = append(rule.destinationAddressItems, item) rule.destinationAddressItems = append(rule.destinationAddressItems, item)
rule.allItems = append(rule.allItems, item) rule.allItems = append(rule.allItems, item)
@ -246,6 +246,26 @@ func NewDefaultRule(ctx context.Context, logger log.ContextLogger, options optio
rule.items = append(rule.items, item) rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item) rule.allItems = append(rule.allItems, item)
} }
if options.InterfaceAddress != nil && options.InterfaceAddress.Size() > 0 {
item := NewInterfaceAddressItem(networkManager, options.InterfaceAddress)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if options.NetworkInterfaceAddress != nil && options.NetworkInterfaceAddress.Size() > 0 {
item := NewNetworkInterfaceAddressItem(networkManager, options.NetworkInterfaceAddress)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.DefaultInterfaceAddress) > 0 {
item := NewDefaultInterfaceAddressItem(networkManager, options.DefaultInterfaceAddress)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.PreferredBy) > 0 {
item := NewPreferredByItem(ctx, options.PreferredBy)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.RuleSet) > 0 { if len(options.RuleSet) > 0 {
var matchSource bool var matchSource bool
if options.RuleSetIPCIDRMatchSource { if options.RuleSetIPCIDRMatchSource {

View File

@ -0,0 +1,56 @@
package rule
import (
"net/netip"
"strings"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/json/badoption"
)
var _ RuleItem = (*DefaultInterfaceAddressItem)(nil)
type DefaultInterfaceAddressItem struct {
interfaceMonitor tun.DefaultInterfaceMonitor
interfaceAddresses []netip.Prefix
}
func NewDefaultInterfaceAddressItem(networkManager adapter.NetworkManager, interfaceAddresses badoption.Listable[badoption.Prefixable]) *DefaultInterfaceAddressItem {
item := &DefaultInterfaceAddressItem{
interfaceMonitor: networkManager.InterfaceMonitor(),
interfaceAddresses: make([]netip.Prefix, 0, len(interfaceAddresses)),
}
for _, prefixable := range interfaceAddresses {
item.interfaceAddresses = append(item.interfaceAddresses, prefixable.Build(netip.Prefix{}))
}
return item
}
func (r *DefaultInterfaceAddressItem) Match(metadata *adapter.InboundContext) bool {
defaultInterface := r.interfaceMonitor.DefaultInterface()
if defaultInterface == nil {
return false
}
for _, address := range r.interfaceAddresses {
if common.All(defaultInterface.Addresses, func(it netip.Prefix) bool {
return !address.Overlaps(it)
}) {
return false
}
}
return true
}
func (r *DefaultInterfaceAddressItem) String() string {
addressLen := len(r.interfaceAddresses)
switch {
case addressLen == 1:
return "default_interface_address=" + r.interfaceAddresses[0].String()
case addressLen > 3:
return "default_interface_address=[" + strings.Join(common.Map(r.interfaceAddresses[:3], netip.Prefix.String), " ") + "...]"
default:
return "default_interface_address=[" + strings.Join(common.Map(r.interfaceAddresses, netip.Prefix.String), " ") + "]"
}
}

View File

@ -247,6 +247,21 @@ func NewDefaultDNSRule(ctx context.Context, logger log.ContextLogger, options op
rule.items = append(rule.items, item) rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item) rule.allItems = append(rule.allItems, item)
} }
if options.InterfaceAddress != nil && options.InterfaceAddress.Size() > 0 {
item := NewInterfaceAddressItem(networkManager, options.InterfaceAddress)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if options.NetworkInterfaceAddress != nil && options.NetworkInterfaceAddress.Size() > 0 {
item := NewNetworkInterfaceAddressItem(networkManager, options.NetworkInterfaceAddress)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.DefaultInterfaceAddress) > 0 {
item := NewDefaultInterfaceAddressItem(networkManager, options.DefaultInterfaceAddress)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.RuleSet) > 0 { if len(options.RuleSet) > 0 {
var matchSource bool var matchSource bool
if options.RuleSetIPCIDRMatchSource { if options.RuleSetIPCIDRMatchSource {

View File

@ -164,13 +164,21 @@ func NewDefaultHeadlessRule(ctx context.Context, options option.DefaultHeadlessR
item := NewWIFISSIDItem(networkManager, options.WIFISSID) item := NewWIFISSIDItem(networkManager, options.WIFISSID)
rule.items = append(rule.items, item) rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item) rule.allItems = append(rule.allItems, item)
} }
if len(options.WIFIBSSID) > 0 { if len(options.WIFIBSSID) > 0 {
item := NewWIFIBSSIDItem(networkManager, options.WIFIBSSID) item := NewWIFIBSSIDItem(networkManager, options.WIFIBSSID)
rule.items = append(rule.items, item) rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item) rule.allItems = append(rule.allItems, item)
}
if options.NetworkInterfaceAddress != nil && options.NetworkInterfaceAddress.Size() > 0 {
item := NewNetworkInterfaceAddressItem(networkManager, options.NetworkInterfaceAddress)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.DefaultInterfaceAddress) > 0 {
item := NewDefaultInterfaceAddressItem(networkManager, options.DefaultInterfaceAddress)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
} }
} }
if len(options.AdGuardDomain) > 0 { if len(options.AdGuardDomain) > 0 {

View File

@ -0,0 +1,62 @@
package rule
import (
"net/netip"
"strings"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/control"
"github.com/sagernet/sing/common/json/badjson"
"github.com/sagernet/sing/common/json/badoption"
)
var _ RuleItem = (*InterfaceAddressItem)(nil)
type InterfaceAddressItem struct {
networkManager adapter.NetworkManager
interfaceAddresses map[string][]netip.Prefix
description string
}
func NewInterfaceAddressItem(networkManager adapter.NetworkManager, interfaceAddresses *badjson.TypedMap[string, badoption.Listable[badoption.Prefixable]]) *InterfaceAddressItem {
item := &InterfaceAddressItem{
networkManager: networkManager,
interfaceAddresses: make(map[string][]netip.Prefix, interfaceAddresses.Size()),
}
var entryDescriptions []string
for _, entry := range interfaceAddresses.Entries() {
prefixes := make([]netip.Prefix, 0, len(entry.Value))
for _, prefixable := range entry.Value {
prefixes = append(prefixes, prefixable.Build(netip.Prefix{}))
}
item.interfaceAddresses[entry.Key] = prefixes
entryDescriptions = append(entryDescriptions, entry.Key+"="+strings.Join(common.Map(prefixes, netip.Prefix.String), ","))
}
item.description = "interface_address=[" + strings.Join(entryDescriptions, " ") + "]"
return item
}
func (r *InterfaceAddressItem) Match(metadata *adapter.InboundContext) bool {
interfaces := r.networkManager.InterfaceFinder().Interfaces()
for ifName, addresses := range r.interfaceAddresses {
iface := common.Find(interfaces, func(it control.Interface) bool {
return it.Name == ifName
})
if iface.Name == "" {
return false
}
if common.All(addresses, func(address netip.Prefix) bool {
return common.All(iface.Addresses, func(it netip.Prefix) bool {
return !address.Overlaps(it)
})
}) {
return false
}
}
return true
}
func (r *InterfaceAddressItem) String() string {
return r.description
}

View File

@ -0,0 +1,86 @@
package rule
import (
"context"
"strings"
"github.com/sagernet/sing-box/adapter"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/service"
)
var _ RuleItem = (*PreferredByItem)(nil)
type PreferredByItem struct {
ctx context.Context
outboundTags []string
outbounds []adapter.OutboundWithPreferredRoutes
}
func NewPreferredByItem(ctx context.Context, outboundTags []string) *PreferredByItem {
return &PreferredByItem{
ctx: ctx,
outboundTags: outboundTags,
}
}
func (r *PreferredByItem) Start() error {
outboundManager := service.FromContext[adapter.OutboundManager](r.ctx)
for _, outboundTag := range r.outboundTags {
rawOutbound, loaded := outboundManager.Outbound(outboundTag)
if !loaded {
return E.New("outbound not found: ", outboundTag)
}
outboundWithPreferredRoutes, withRoutes := rawOutbound.(adapter.OutboundWithPreferredRoutes)
if !withRoutes {
return E.New("outbound type does not support preferred routes: ", rawOutbound.Type())
}
r.outbounds = append(r.outbounds, outboundWithPreferredRoutes)
}
return nil
}
func (r *PreferredByItem) Match(metadata *adapter.InboundContext) bool {
var domainHost string
if metadata.Domain != "" {
domainHost = metadata.Domain
} else {
domainHost = metadata.Destination.Fqdn
}
if domainHost != "" {
for _, outbound := range r.outbounds {
if outbound.PreferredDomain(domainHost) {
return true
}
}
}
if metadata.Destination.IsIP() {
for _, outbound := range r.outbounds {
if outbound.PreferredAddress(metadata.Destination.Addr) {
return true
}
}
}
if len(metadata.DestinationAddresses) > 0 {
for _, address := range metadata.DestinationAddresses {
for _, outbound := range r.outbounds {
if outbound.PreferredAddress(address) {
return true
}
}
}
}
return false
}
func (r *PreferredByItem) String() string {
description := "preferred_by="
pLen := len(r.outboundTags)
if pLen == 1 {
description += F.ToString(r.outboundTags[0])
} else {
description += "[" + strings.Join(F.MapToString(r.outboundTags), " ") + "]"
}
return description
}

View File

@ -0,0 +1,64 @@
package rule
import (
"net/netip"
"strings"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/json/badjson"
"github.com/sagernet/sing/common/json/badoption"
)
var _ RuleItem = (*NetworkInterfaceAddressItem)(nil)
type NetworkInterfaceAddressItem struct {
networkManager adapter.NetworkManager
interfaceAddresses map[C.InterfaceType][]netip.Prefix
description string
}
func NewNetworkInterfaceAddressItem(networkManager adapter.NetworkManager, interfaceAddresses *badjson.TypedMap[option.InterfaceType, badoption.Listable[badoption.Prefixable]]) *NetworkInterfaceAddressItem {
item := &NetworkInterfaceAddressItem{
networkManager: networkManager,
interfaceAddresses: make(map[C.InterfaceType][]netip.Prefix, interfaceAddresses.Size()),
}
var entryDescriptions []string
for _, entry := range interfaceAddresses.Entries() {
prefixes := make([]netip.Prefix, 0, len(entry.Value))
for _, prefixable := range entry.Value {
prefixes = append(prefixes, prefixable.Build(netip.Prefix{}))
}
item.interfaceAddresses[entry.Key.Build()] = prefixes
entryDescriptions = append(entryDescriptions, entry.Key.Build().String()+"="+strings.Join(common.Map(prefixes, netip.Prefix.String), ","))
}
item.description = "network_interface_address=[" + strings.Join(entryDescriptions, " ") + "]"
return item
}
func (r *NetworkInterfaceAddressItem) Match(metadata *adapter.InboundContext) bool {
interfaces := r.networkManager.NetworkInterfaces()
match:
for ifType, addresses := range r.interfaceAddresses {
for _, networkInterface := range interfaces {
if networkInterface.Type != ifType {
continue
}
if common.Any(networkInterface.Addresses, func(it netip.Prefix) bool {
return common.Any(addresses, func(prefix netip.Prefix) bool {
return prefix.Overlaps(it)
})
}) {
continue match
}
}
return false
}
return true
}
func (r *NetworkInterfaceAddressItem) String() string {
return r.description
}

View File

@ -42,7 +42,7 @@ func extractIPSetFromRule(rawRule adapter.HeadlessRule) []*netipx.IPSet {
} }
} }
func hasHeadlessRule(rules []option.HeadlessRule, cond func(rule option.DefaultHeadlessRule) bool) bool { func HasHeadlessRule(rules []option.HeadlessRule, cond func(rule option.DefaultHeadlessRule) bool) bool {
for _, rule := range rules { for _, rule := range rules {
switch rule.Type { switch rule.Type {
case C.RuleTypeDefault: case C.RuleTypeDefault:
@ -50,7 +50,7 @@ func hasHeadlessRule(rules []option.HeadlessRule, cond func(rule option.DefaultH
return true return true
} }
case C.RuleTypeLogical: case C.RuleTypeLogical:
if hasHeadlessRule(rule.LogicalOptions.Rules, cond) { if HasHeadlessRule(rule.LogicalOptions.Rules, cond) {
return true return true
} }
} }

View File

@ -138,9 +138,9 @@ func (s *LocalRuleSet) reloadRules(headlessRules []option.HeadlessRule) error {
} }
} }
var metadata adapter.RuleSetMetadata var metadata adapter.RuleSetMetadata
metadata.ContainsProcessRule = hasHeadlessRule(headlessRules, isProcessHeadlessRule) metadata.ContainsProcessRule = HasHeadlessRule(headlessRules, isProcessHeadlessRule)
metadata.ContainsWIFIRule = hasHeadlessRule(headlessRules, isWIFIHeadlessRule) metadata.ContainsWIFIRule = HasHeadlessRule(headlessRules, isWIFIHeadlessRule)
metadata.ContainsIPCIDRRule = hasHeadlessRule(headlessRules, isIPCIDRHeadlessRule) metadata.ContainsIPCIDRRule = HasHeadlessRule(headlessRules, isIPCIDRHeadlessRule)
s.rules = rules s.rules = rules
s.metadata = metadata s.metadata = metadata
s.callbackAccess.Lock() s.callbackAccess.Lock()

View File

@ -185,9 +185,9 @@ func (s *RemoteRuleSet) loadBytes(content []byte) error {
return E.Cause(err, "parse rule_set.rules.[", i, "]") return E.Cause(err, "parse rule_set.rules.[", i, "]")
} }
} }
s.metadata.ContainsProcessRule = hasHeadlessRule(plainRuleSet.Rules, isProcessHeadlessRule) s.metadata.ContainsProcessRule = HasHeadlessRule(plainRuleSet.Rules, isProcessHeadlessRule)
s.metadata.ContainsWIFIRule = hasHeadlessRule(plainRuleSet.Rules, isWIFIHeadlessRule) s.metadata.ContainsWIFIRule = HasHeadlessRule(plainRuleSet.Rules, isWIFIHeadlessRule)
s.metadata.ContainsIPCIDRRule = hasHeadlessRule(plainRuleSet.Rules, isIPCIDRHeadlessRule) s.metadata.ContainsIPCIDRRule = HasHeadlessRule(plainRuleSet.Rules, isIPCIDRHeadlessRule)
s.rules = rules s.rules = rules
s.callbackAccess.Lock() s.callbackAccess.Lock()
callbacks := s.callbacks.Array() callbacks := s.callbacks.Array()

View File

@ -156,18 +156,14 @@ func (s *APIServer) deleteUser(writer http.ResponseWriter, request *http.Request
} }
func (s *APIServer) getStats(writer http.ResponseWriter, request *http.Request) { func (s *APIServer) getStats(writer http.ResponseWriter, request *http.Request) {
requireClear := chi.URLParam(request, "clear") == "true" requireClear := request.URL.Query().Get("clear") == "true"
users := s.user.List() users := s.user.List()
s.traffic.ReadUsers(users) s.traffic.ReadUsers(users, requireClear)
for i := range users { for i := range users {
users[i].Password = "" users[i].Password = ""
} }
uplinkBytes, downlinkBytes, uplinkPackets, downlinkPackets, tcpSessions, udpSessions := s.traffic.ReadGlobal() uplinkBytes, downlinkBytes, uplinkPackets, downlinkPackets, tcpSessions, udpSessions := s.traffic.ReadGlobal(requireClear)
if requireClear {
s.traffic.Clear()
}
render.JSON(writer, request, render.M{ render.JSON(writer, request, render.M{
"uplinkBytes": uplinkBytes, "uplinkBytes": uplinkBytes,

View File

@ -142,85 +142,82 @@ func (s *TrafficManager) TrackPacketConnection(conn N.PacketConn, metadata adapt
readPacketCounter = append(readPacketCounter, upPacketsCounter) readPacketCounter = append(readPacketCounter, upPacketsCounter)
writePacketCounter = append(writePacketCounter, downPacketsCounter) writePacketCounter = append(writePacketCounter, downPacketsCounter)
udpSessionCounter.Add(1) udpSessionCounter.Add(1)
return bufio.NewInt64CounterPacketConn(conn, append(readCounter, readPacketCounter...), append(writeCounter, writePacketCounter...)) return bufio.NewInt64CounterPacketConn(conn, readCounter, readPacketCounter, writeCounter, writePacketCounter)
} }
func (s *TrafficManager) ReadUser(user *UserObject) { func (s *TrafficManager) ReadUser(user *UserObject) {
s.userAccess.Lock() s.userAccess.Lock()
defer s.userAccess.Unlock() defer s.userAccess.Unlock()
s.readUser(user) s.readUser(user, false)
} }
func (s *TrafficManager) readUser(user *UserObject) { func (s *TrafficManager) readUser(user *UserObject, swap bool) {
if counter, loaded := s.userUplink[user.UserName]; loaded { if counter, loaded := s.userUplink[user.UserName]; loaded {
user.UplinkBytes = counter.Load() if swap {
user.UplinkBytes = counter.Swap(0)
} else {
user.UplinkBytes = counter.Load()
}
} }
if counter, loaded := s.userDownlink[user.UserName]; loaded { if counter, loaded := s.userDownlink[user.UserName]; loaded {
user.DownlinkBytes = counter.Load() if swap {
user.DownlinkBytes = counter.Swap(0)
} else {
user.DownlinkBytes = counter.Load()
}
} }
if counter, loaded := s.userUplinkPackets[user.UserName]; loaded { if counter, loaded := s.userUplinkPackets[user.UserName]; loaded {
user.UplinkPackets = counter.Load() if swap {
user.UplinkPackets = counter.Swap(0)
} else {
user.UplinkPackets = counter.Load()
}
} }
if counter, loaded := s.userDownlinkPackets[user.UserName]; loaded { if counter, loaded := s.userDownlinkPackets[user.UserName]; loaded {
user.DownlinkPackets = counter.Load() if swap {
user.DownlinkPackets = counter.Swap(0)
} else {
user.DownlinkPackets = counter.Load()
}
} }
if counter, loaded := s.userTCPSessions[user.UserName]; loaded { if counter, loaded := s.userTCPSessions[user.UserName]; loaded {
user.TCPSessions = counter.Load() if swap {
user.TCPSessions = counter.Swap(0)
} else {
user.TCPSessions = counter.Load()
}
} }
if counter, loaded := s.userUDPSessions[user.UserName]; loaded { if counter, loaded := s.userUDPSessions[user.UserName]; loaded {
user.UDPSessions = counter.Load() if swap {
user.UDPSessions = counter.Swap(0)
} else {
user.UDPSessions = counter.Load()
}
} }
} }
func (s *TrafficManager) ReadUsers(users []*UserObject) { func (s *TrafficManager) ReadUsers(users []*UserObject, swap bool) {
s.userAccess.Lock() s.userAccess.Lock()
defer s.userAccess.Unlock() defer s.userAccess.Unlock()
for _, user := range users { for _, user := range users {
s.readUser(user) s.readUser(user, swap)
} }
} }
func (s *TrafficManager) ReadGlobal() ( func (s *TrafficManager) ReadGlobal(swap bool) (uplinkBytes int64, downlinkBytes int64, uplinkPackets int64, downlinkPackets int64, tcpSessions int64, udpSessions int64) {
uplinkBytes int64, if swap {
downlinkBytes int64, return s.globalUplink.Swap(0),
uplinkPackets int64, s.globalDownlink.Swap(0),
downlinkPackets int64, s.globalUplinkPackets.Swap(0),
tcpSessions int64, s.globalDownlinkPackets.Swap(0),
udpSessions int64, s.globalTCPSessions.Swap(0),
) { s.globalUDPSessions.Swap(0)
return s.globalUplink.Load(), } else {
s.globalDownlink.Load(), return s.globalUplink.Load(),
s.globalUplinkPackets.Load(), s.globalDownlink.Load(),
s.globalDownlinkPackets.Load(), s.globalUplinkPackets.Load(),
s.globalTCPSessions.Load(), s.globalDownlinkPackets.Load(),
s.globalUDPSessions.Load() s.globalTCPSessions.Load(),
} s.globalUDPSessions.Load()
func (s *TrafficManager) Clear() {
s.globalUplink.Store(0)
s.globalDownlink.Store(0)
s.globalUplinkPackets.Store(0)
s.globalDownlinkPackets.Store(0)
s.globalTCPSessions.Store(0)
s.globalUDPSessions.Store(0)
s.userAccess.Lock()
defer s.userAccess.Unlock()
for _, counter := range s.userUplink {
counter.Store(0)
}
for _, counter := range s.userDownlink {
counter.Store(0)
}
for _, counter := range s.userUplinkPackets {
counter.Store(0)
}
for _, counter := range s.userDownlinkPackets {
counter.Store(0)
}
for _, counter := range s.userTCPSessions {
counter.Store(0)
}
for _, counter := range s.userUDPSessions {
counter.Store(0)
} }
} }

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"net" "net"
"sync" "sync"
"sync/atomic"
"time" "time"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
@ -29,7 +30,7 @@ type Client struct {
serverAddr string serverAddr string
serviceName string serviceName string
dialOptions []grpc.DialOption dialOptions []grpc.DialOption
conn *grpc.ClientConn conn atomic.Pointer[grpc.ClientConn]
connAccess sync.Mutex connAccess sync.Mutex
} }
@ -74,13 +75,13 @@ func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, opt
} }
func (c *Client) connect() (*grpc.ClientConn, error) { func (c *Client) connect() (*grpc.ClientConn, error) {
conn := c.conn conn := c.conn.Load()
if conn != nil && conn.GetState() != connectivity.Shutdown { if conn != nil && conn.GetState() != connectivity.Shutdown {
return conn, nil return conn, nil
} }
c.connAccess.Lock() c.connAccess.Lock()
defer c.connAccess.Unlock() defer c.connAccess.Unlock()
conn = c.conn conn = c.conn.Load()
if conn != nil && conn.GetState() != connectivity.Shutdown { if conn != nil && conn.GetState() != connectivity.Shutdown {
return conn, nil return conn, nil
} }
@ -89,7 +90,7 @@ func (c *Client) connect() (*grpc.ClientConn, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
c.conn = conn c.conn.Store(conn)
return conn, nil return conn, nil
} }
@ -109,11 +110,9 @@ func (c *Client) DialContext(ctx context.Context) (net.Conn, error) {
} }
func (c *Client) Close() error { func (c *Client) Close() error {
c.connAccess.Lock() conn := c.conn.Swap(nil)
defer c.connAccess.Unlock() if conn != nil {
if c.conn != nil { conn.Close()
c.conn.Close()
c.conn = nil
} }
return nil return nil
} }

View File

@ -29,7 +29,6 @@ var defaultClientHeader = http.Header{
type Client struct { type Client struct {
ctx context.Context ctx context.Context
dialer N.Dialer
serverAddr M.Socksaddr serverAddr M.Socksaddr
transport *http2.Transport transport *http2.Transport
options option.V2RayGRPCOptions options option.V2RayGRPCOptions
@ -46,7 +45,6 @@ func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, opt
} }
client := &Client{ client := &Client{
ctx: ctx, ctx: ctx,
dialer: dialer,
serverAddr: serverAddr, serverAddr: serverAddr,
options: options, options: options,
transport: &http2.Transport{ transport: &http2.Transport{
@ -62,7 +60,6 @@ func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, opt
}, },
host: host, host: host,
} }
if tlsConfig == nil { if tlsConfig == nil {
client.transport.DialTLSContext = func(ctx context.Context, network, addr string, cfg *tls.STDConfig) (net.Conn, error) { client.transport.DialTLSContext = func(ctx context.Context, network, addr string, cfg *tls.STDConfig) (net.Conn, error) {
return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
@ -71,12 +68,9 @@ func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, opt
if len(tlsConfig.NextProtos()) == 0 { if len(tlsConfig.NextProtos()) == 0 {
tlsConfig.SetNextProtos([]string{http2.NextProtoTLS}) tlsConfig.SetNextProtos([]string{http2.NextProtoTLS})
} }
tlsDialer := tls.NewDialer(dialer, tlsConfig)
client.transport.DialTLSContext = func(ctx context.Context, network, addr string, cfg *tls.STDConfig) (net.Conn, error) { client.transport.DialTLSContext = func(ctx context.Context, network, addr string, cfg *tls.STDConfig) (net.Conn, error) {
conn, err := dialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) return tlsDialer.DialTLSContext(ctx, M.ParseSocksaddr(addr))
if err != nil {
return nil, err
}
return tls.ClientHandshake(ctx, conn, tlsConfig)
} }
} }

View File

@ -47,15 +47,12 @@ func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, opt
if len(tlsConfig.NextProtos()) == 0 { if len(tlsConfig.NextProtos()) == 0 {
tlsConfig.SetNextProtos([]string{http2.NextProtoTLS}) tlsConfig.SetNextProtos([]string{http2.NextProtoTLS})
} }
tlsDialer := tls.NewDialer(dialer, tlsConfig)
transport = &http2.Transport{ transport = &http2.Transport{
ReadIdleTimeout: time.Duration(options.IdleTimeout), ReadIdleTimeout: time.Duration(options.IdleTimeout),
PingTimeout: time.Duration(options.PingTimeout), PingTimeout: time.Duration(options.PingTimeout),
DialTLSContext: func(ctx context.Context, network, addr string, cfg *tls.STDConfig) (net.Conn, error) { DialTLSContext: func(ctx context.Context, network, addr string, cfg *tls.STDConfig) (net.Conn, error) {
conn, err := dialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) return tlsDialer.DialTLSContext(ctx, M.ParseSocksaddr(addr))
if err != nil {
return nil, err
}
return tls.ClientHandshake(ctx, conn, tlsConfig)
}, },
} }
} }

View File

@ -23,7 +23,6 @@ var _ adapter.V2RayClientTransport = (*Client)(nil)
type Client struct { type Client struct {
dialer N.Dialer dialer N.Dialer
tlsConfig tls.Config
serverAddr M.Socksaddr serverAddr M.Socksaddr
requestURL url.URL requestURL url.URL
headers http.Header headers http.Header
@ -35,6 +34,7 @@ func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, opt
if len(tlsConfig.NextProtos()) == 0 { if len(tlsConfig.NextProtos()) == 0 {
tlsConfig.SetNextProtos([]string{"http/1.1"}) tlsConfig.SetNextProtos([]string{"http/1.1"})
} }
dialer = tls.NewDialer(dialer, tlsConfig)
} }
var host string var host string
if options.Host != "" { if options.Host != "" {
@ -65,7 +65,6 @@ func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, opt
} }
return &Client{ return &Client{
dialer: dialer, dialer: dialer,
tlsConfig: tlsConfig,
serverAddr: serverAddr, serverAddr: serverAddr,
requestURL: requestURL, requestURL: requestURL,
headers: headers, headers: headers,
@ -78,12 +77,6 @@ func (c *Client) DialContext(ctx context.Context) (net.Conn, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
if c.tlsConfig != nil {
conn, err = tls.ClientHandshake(ctx, conn, c.tlsConfig)
if err != nil {
return nil, err
}
}
request := &http.Request{ request := &http.Request{
Method: http.MethodGet, Method: http.MethodGet,
URL: &c.requestURL, URL: &c.requestURL,

View File

@ -15,6 +15,7 @@ import (
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-quic" "github.com/sagernet/sing-quic"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/atomic"
"github.com/sagernet/sing/common/bufio" "github.com/sagernet/sing/common/bufio"
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"
@ -29,7 +30,7 @@ type Client struct {
tlsConfig tls.Config tlsConfig tls.Config
quicConfig *quic.Config quicConfig *quic.Config
connAccess sync.Mutex connAccess sync.Mutex
conn quic.Connection conn atomic.TypedValue[quic.Connection]
rawConn net.Conn rawConn net.Conn
} }
@ -50,13 +51,13 @@ func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, opt
} }
func (c *Client) offer() (quic.Connection, error) { func (c *Client) offer() (quic.Connection, error) {
conn := c.conn conn := c.conn.Load()
if conn != nil && !common.Done(conn.Context()) { if conn != nil && !common.Done(conn.Context()) {
return conn, nil return conn, nil
} }
c.connAccess.Lock() c.connAccess.Lock()
defer c.connAccess.Unlock() defer c.connAccess.Unlock()
conn = c.conn conn = c.conn.Load()
if conn != nil && !common.Done(conn.Context()) { if conn != nil && !common.Done(conn.Context()) {
return conn, nil return conn, nil
} }
@ -78,7 +79,7 @@ func (c *Client) offerNew() (quic.Connection, error) {
packetConn.Close() packetConn.Close()
return nil, err return nil, err
} }
c.conn = quicConn c.conn.Store(quicConn)
c.rawConn = udpConn c.rawConn = udpConn
return quicConn, nil return quicConn, nil
} }
@ -98,13 +99,13 @@ func (c *Client) DialContext(ctx context.Context) (net.Conn, error) {
func (c *Client) Close() error { func (c *Client) Close() error {
c.connAccess.Lock() c.connAccess.Lock()
defer c.connAccess.Unlock() defer c.connAccess.Unlock()
if c.conn != nil { conn := c.conn.Swap(nil)
c.conn.CloseWithError(0, "") if conn != nil {
conn.CloseWithError(0, "")
} }
if c.rawConn != nil { if c.rawConn != nil {
c.rawConn.Close() c.rawConn.Close()
} }
c.conn = nil
c.rawConn = nil c.rawConn = nil
return nil return nil
} }

View File

@ -26,7 +26,6 @@ var _ adapter.V2RayClientTransport = (*Client)(nil)
type Client struct { type Client struct {
dialer N.Dialer dialer N.Dialer
tlsConfig tls.Config
serverAddr M.Socksaddr serverAddr M.Socksaddr
requestURL url.URL requestURL url.URL
headers http.Header headers http.Header
@ -39,6 +38,7 @@ func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, opt
if len(tlsConfig.NextProtos()) == 0 { if len(tlsConfig.NextProtos()) == 0 {
tlsConfig.SetNextProtos([]string{"http/1.1"}) tlsConfig.SetNextProtos([]string{"http/1.1"})
} }
dialer = tls.NewDialer(dialer, tlsConfig)
} }
var requestURL url.URL var requestURL url.URL
if tlsConfig == nil { if tlsConfig == nil {
@ -65,7 +65,6 @@ func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, opt
} }
return &Client{ return &Client{
dialer, dialer,
tlsConfig,
serverAddr, serverAddr,
requestURL, requestURL,
headers, headers,
@ -79,12 +78,6 @@ func (c *Client) dialContext(ctx context.Context, requestURL *url.URL, headers h
if err != nil { if err != nil {
return nil, err return nil, err
} }
if c.tlsConfig != nil {
conn, err = tls.ClientHandshake(ctx, conn, c.tlsConfig)
if err != nil {
return nil, err
}
}
var deadlineConn net.Conn var deadlineConn net.Conn
if deadline.NeedAdditionalReadDeadline(conn) { if deadline.NeedAdditionalReadDeadline(conn) {
deadlineConn = deadline.NewConn(conn) deadlineConn = deadline.NewConn(conn)

View File

@ -8,6 +8,7 @@ import (
"net" "net"
"os" "os"
"sync" "sync"
"sync/atomic"
"time" "time"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
@ -135,20 +136,22 @@ func (c *WebsocketConn) Upstream() any {
type EarlyWebsocketConn struct { type EarlyWebsocketConn struct {
*Client *Client
ctx context.Context ctx context.Context
conn *WebsocketConn conn atomic.Pointer[WebsocketConn]
access sync.Mutex access sync.Mutex
create chan struct{} create chan struct{}
err error err error
} }
func (c *EarlyWebsocketConn) Read(b []byte) (n int, err error) { func (c *EarlyWebsocketConn) Read(b []byte) (n int, err error) {
if c.conn == nil { conn := c.conn.Load()
if conn == nil {
<-c.create <-c.create
if c.err != nil { if c.err != nil {
return 0, c.err return 0, c.err
} }
conn = c.conn.Load()
} }
return wrapWsError0(c.conn.Read(b)) return wrapWsError0(conn.Read(b))
} }
func (c *EarlyWebsocketConn) writeRequest(content []byte) error { func (c *EarlyWebsocketConn) writeRequest(content []byte) error {
@ -187,21 +190,23 @@ func (c *EarlyWebsocketConn) writeRequest(content []byte) error {
return err return err
} }
} }
c.conn = conn c.conn.Store(conn)
return nil return nil
} }
func (c *EarlyWebsocketConn) Write(b []byte) (n int, err error) { func (c *EarlyWebsocketConn) Write(b []byte) (n int, err error) {
if c.conn != nil { conn := c.conn.Load()
return wrapWsError0(c.conn.Write(b)) if conn != nil {
return wrapWsError0(conn.Write(b))
} }
c.access.Lock() c.access.Lock()
defer c.access.Unlock() defer c.access.Unlock()
conn = c.conn.Load()
if c.err != nil { if c.err != nil {
return 0, c.err return 0, c.err
} }
if c.conn != nil { if conn != nil {
return wrapWsError0(c.conn.Write(b)) return wrapWsError0(conn.Write(b))
} }
err = c.writeRequest(b) err = c.writeRequest(b)
c.err = err c.err = err
@ -213,17 +218,19 @@ 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 { conn := c.conn.Load()
return wrapWsError(c.conn.WriteBuffer(buffer)) if conn != nil {
return wrapWsError(conn.WriteBuffer(buffer))
} }
c.access.Lock() c.access.Lock()
defer c.access.Unlock() defer c.access.Unlock()
if c.conn != nil {
return wrapWsError(c.conn.WriteBuffer(buffer))
}
if c.err != nil { if c.err != nil {
return c.err return c.err
} }
conn = c.conn.Load()
if conn != nil {
return wrapWsError(conn.WriteBuffer(buffer))
}
err := c.writeRequest(buffer.Bytes()) err := c.writeRequest(buffer.Bytes())
c.err = err c.err = err
close(c.create) close(c.create)
@ -231,24 +238,27 @@ func (c *EarlyWebsocketConn) WriteBuffer(buffer *buf.Buffer) error {
} }
func (c *EarlyWebsocketConn) Close() error { func (c *EarlyWebsocketConn) Close() error {
if c.conn == nil { conn := c.conn.Load()
if conn == nil {
return nil return nil
} }
return c.conn.Close() return conn.Close()
} }
func (c *EarlyWebsocketConn) LocalAddr() net.Addr { func (c *EarlyWebsocketConn) LocalAddr() net.Addr {
if c.conn == nil { conn := c.conn.Load()
if conn == nil {
return M.Socksaddr{} return M.Socksaddr{}
} }
return c.conn.LocalAddr() return conn.LocalAddr()
} }
func (c *EarlyWebsocketConn) RemoteAddr() net.Addr { func (c *EarlyWebsocketConn) RemoteAddr() net.Addr {
if c.conn == nil { conn := c.conn.Load()
if conn == nil {
return M.Socksaddr{} return M.Socksaddr{}
} }
return c.conn.RemoteAddr() return conn.RemoteAddr()
} }
func (c *EarlyWebsocketConn) SetDeadline(t time.Time) error { func (c *EarlyWebsocketConn) SetDeadline(t time.Time) error {
@ -268,11 +278,11 @@ func (c *EarlyWebsocketConn) NeedAdditionalReadDeadline() bool {
} }
func (c *EarlyWebsocketConn) Upstream() any { func (c *EarlyWebsocketConn) Upstream() any {
return common.PtrOrNil(c.conn) return common.PtrOrNil(c.conn.Load())
} }
func (c *EarlyWebsocketConn) LazyHeadroom() bool { func (c *EarlyWebsocketConn) LazyHeadroom() bool {
return c.conn == nil return c.conn.Load() == nil
} }
func wrapWsError(err error) error { func wrapWsError(err error) error {

View File

@ -8,7 +8,9 @@ import (
"net" "net"
"net/netip" "net/netip"
"os" "os"
"reflect"
"strings" "strings"
"unsafe"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
@ -30,6 +32,7 @@ type Endpoint struct {
allowedAddress []netip.Prefix allowedAddress []netip.Prefix
tunDevice Device tunDevice Device
device *device.Device device *device.Device
allowedIPs *device.AllowedIPs
pause pause.Manager pause pause.Manager
pauseCallback *list.Element[pause.Callback] pauseCallback *list.Element[pause.Callback]
} }
@ -191,6 +194,7 @@ func (e *Endpoint) Start(resolve bool) error {
if e.pause != nil { if e.pause != nil {
e.pauseCallback = e.pause.RegisterCallback(e.onPauseUpdated) e.pauseCallback = e.pause.RegisterCallback(e.onPauseUpdated)
} }
e.allowedIPs = (*device.AllowedIPs)(unsafe.Pointer(reflect.Indirect(reflect.ValueOf(wgDevice)).FieldByName("allowedips").UnsafeAddr()))
return nil return nil
} }
@ -218,6 +222,10 @@ func (e *Endpoint) Close() error {
return nil return nil
} }
func (e *Endpoint) Lookup(address netip.Addr) *device.Peer {
return e.allowedIPs.Lookup(address.AsSlice())
}
func (e *Endpoint) onPauseUpdated(event int) { func (e *Endpoint) onPauseUpdated(event int) {
switch event { switch event {
case pause.EventDevicePaused, pause.EventNetworkPause: case pause.EventDevicePaused, pause.EventNetworkPause: