Compare commits

..

37 Commits

Author SHA1 Message Date
世界
0bb28ff4f1 documentation: Bump version 2025-09-13 03:16:25 +08:00
世界
35fc76f2c9 Fix ping domain 2025-09-13 03:16:16 +08:00
世界
dcbf54e1ec release: Improve publish testflight 2025-09-13 03:16:16 +08:00
世界
a28ab43498 release: Fix linux build 2025-09-13 03:16:15 +08:00
世界
6e5b9922de Improve ktls rx error handling 2025-09-13 03:16:15 +08:00
世界
e366c7e2fd Improve compatibility for kTLS 2025-09-13 03:16:14 +08:00
世界
ff420c86fa ktls: Add warning for inappropriate scenarios 2025-09-13 03:16:14 +08:00
世界
6cad62af81 Add support for kTLS
Reference: https://gitlab.com/go-extension/tls
2025-09-13 03:16:14 +08:00
世界
4a9dbe540a Add proxy support for ICMP echo request 2025-09-13 03:16:14 +08:00
世界
fc9d5bb519 Fix resolve using resolved 2025-09-13 03:16:13 +08:00
世界
ba9c4bb1b7 documentation: Update behavior of local DNS server on darwin 2025-09-13 03:16:13 +08:00
世界
facf0d5eae Stop using DHCP on iOS and tvOS
We do not have the `com.apple.developer.networking.multicast` entitlement and are unable to obtain it for non-technical reasons.
2025-09-13 03:16:12 +08:00
世界
a463d1250e Remove use of ldflags -checklinkname=0 on darwin 2025-09-13 03:16:12 +08:00
世界
dabf180add Fix local DNS server on darwin
We mistakenly believed that `libresolv`'s `search` function worked correctly in NetworkExtension, but it seems only `getaddrinfo` does.

This commit changes the behavior of the `local` DNS server in NetworkExtension to prefer DHCP, falling back to `getaddrinfo` if DHCP servers are unavailable.

It's worth noting that `prefer_go` does not disable DHCP since it respects Dial Fields, but `getaddrinfo` does the opposite. The new behavior only applies to NetworkExtension, not to all scenarios (primarily command-line binaries) as it did previously.

In addition, this commit also improves the DHCP DNS server to use the same robust query logic as `local`.
2025-09-13 03:16:12 +08:00
世界
4e4117cd69 Fix legacy DNS config 2025-09-13 03:16:11 +08:00
世界
042ca947c0 Fix rule-set format 2025-09-13 03:16:11 +08:00
世界
973e016605 documentation: Remove outdated icons 2025-09-13 03:16:11 +08:00
世界
14939be4bf documentation: Improve local DNS server 2025-09-13 03:16:11 +08:00
世界
3478f08ca5 Use libresolv in local DNS server on darwin 2025-09-13 03:16:10 +08:00
世界
f50259934c Use resolved in local DNS server if available 2025-09-13 03:16:10 +08:00
xchacha20-poly1305
f1a8ac9a62 Fix rule set version 2025-09-13 03:16:10 +08:00
世界
685c83c923 documentation: Add preferred_by route rule item 2025-09-13 03:16:09 +08:00
世界
c8990aae7f Add preferred_by route rule item 2025-09-13 03:16:09 +08:00
世界
289705d8a3 documentation: Add interface address rule items 2025-09-13 03:16:09 +08:00
世界
f845099c8d Add interface address rule items 2025-09-13 03:16:09 +08:00
neletor
9a774ae8d6 Add support for ech retry configs 2025-09-13 03:16:09 +08:00
Zephyruso
864ad66e88 Add /dns/flush-clash meta api 2025-09-13 03:16:08 +08:00
世界
ae852e0be4 Bump version 2025-09-13 03:09:10 +08:00
世界
1955002ed8 Do not cache DNS responses with empty answers 2025-09-13 03:04:08 +08:00
世界
44559fb7b9 Bump version 2025-09-13 00:07:57 +08:00
世界
0977c5cf73 release: Disable Apple platform CI builds, since `-allowProvisioningUpdates is broken by Apple 2025-09-13 00:07:57 +08:00
世界
07697bf931 release: Fix xcode build 2025-09-12 22:57:44 +08:00
世界
5d1d1a1456 Fix TCP exchange for local/dhcp DNS servers 2025-09-12 21:58:48 +08:00
世界
146383499e Fix race codes 2025-09-12 21:58:48 +08:00
世界
e81a76fdf9 Fix DNS exchange 2025-09-12 18:05:02 +08:00
世界
de13137418 Fix auto redirect 2025-09-12 11:04:12 +08:00
世界
e42b818c2a Fix dhcp fetch 2025-09-12 11:03:13 +08:00
22 changed files with 334 additions and 165 deletions

View File

@@ -432,7 +432,8 @@ jobs:
SERVICE_ACCOUNT_CREDENTIALS: ${{ secrets.SERVICE_ACCOUNT_CREDENTIALS }}
build_apple:
name: Build Apple clients
runs-on: macos-15
runs-on: macos-26
if: false
needs:
- calculate_version
strategy:
@@ -479,14 +480,6 @@ jobs:
uses: actions/setup-go@v5
with:
go-version: ^1.25.1
- name: Setup Xcode stable
if: matrix.if && github.ref == 'refs/heads/main-next'
run: |-
sudo xcode-select -s /Applications/Xcode_16.4.app
- name: Setup Xcode beta
if: matrix.if && github.ref == 'refs/heads/dev-next'
run: |-
sudo xcode-select -s /Applications/Xcode_16.4.app
- name: Set tag
if: matrix.if
run: |-

View File

@@ -17,6 +17,10 @@ build:
export GOTOOLCHAIN=local && \
go build $(MAIN_PARAMS) $(MAIN)
race:
export GOTOOLCHAIN=local && \
go build -race $(MAIN_PARAMS) $(MAIN)
ci_build:
export GOTOOLCHAIN=local && \
go build $(PARAMS) $(MAIN) && \

View File

@@ -7,6 +7,7 @@ import (
"os"
"path/filepath"
"strings"
"sync"
"github.com/sagernet/fswatch"
"github.com/sagernet/sing-box/adapter"
@@ -21,6 +22,7 @@ import (
var _ adapter.CertificateStore = (*Store)(nil)
type Store struct {
access sync.RWMutex
systemPool *x509.CertPool
currentPool *x509.CertPool
certificate string
@@ -115,10 +117,14 @@ func (s *Store) Close() error {
}
func (s *Store) Pool() *x509.CertPool {
s.access.RLock()
defer s.access.RUnlock()
return s.currentPool
}
func (s *Store) update() error {
s.access.Lock()
defer s.access.Unlock()
var currentPool *x509.CertPool
if s.systemPool == nil {
currentPool = x509.NewCertPool()

View File

@@ -69,11 +69,7 @@ func parseECHServerConfig(ctx context.Context, options option.InboundTLSOptions,
} else {
return E.New("missing ECH keys")
}
block, rest := pem.Decode(echKey)
if block == nil || block.Type != "ECH KEYS" || len(rest) > 0 {
return E.New("invalid ECH keys pem")
}
echKeys, err := UnmarshalECHKeys(block.Bytes)
echKeys, err := parseECHKeys(echKey)
if err != nil {
return E.Cause(err, "parse ECH keys")
}
@@ -85,21 +81,29 @@ func parseECHServerConfig(ctx context.Context, options option.InboundTLSOptions,
return nil
}
func reloadECHKeys(echKeyPath string, tlsConfig *tls.Config) error {
echKey, err := os.ReadFile(echKeyPath)
func (c *STDServerConfig) setECHServerConfig(echKey []byte) error {
echKeys, err := parseECHKeys(echKey)
if err != nil {
return E.Cause(err, "reload ECH keys from ", echKeyPath)
return err
}
c.access.Lock()
config := c.config.Clone()
config.EncryptedClientHelloKeys = echKeys
c.config = config
c.access.Unlock()
return nil
}
func parseECHKeys(echKey []byte) ([]tls.EncryptedClientHelloKey, error) {
block, _ := pem.Decode(echKey)
if block == nil || block.Type != "ECH KEYS" {
return E.New("invalid ECH keys pem")
return nil, E.New("invalid ECH keys pem")
}
echKeys, err := UnmarshalECHKeys(block.Bytes)
if err != nil {
return E.Cause(err, "parse ECH keys")
return nil, E.Cause(err, "parse ECH keys")
}
tlsConfig.EncryptedClientHelloKeys = echKeys
return nil
return echKeys, nil
}
type ECHClientConfig struct {

View File

@@ -18,6 +18,6 @@ func parseECHServerConfig(ctx context.Context, options option.InboundTLSOptions,
return E.New("ECH requires go1.24, please recompile your binary.")
}
func reloadECHKeys(echKeyPath string, tlsConfig *tls.Config) error {
return E.New("ECH requires go1.24, please recompile your binary.")
func (c *STDServerConfig) setECHServerConfig(echKey []byte) error {
panic("unreachable")
}

View File

@@ -6,6 +6,7 @@ import (
"net"
"os"
"strings"
"sync"
"time"
"github.com/sagernet/fswatch"
@@ -21,6 +22,7 @@ import (
var errInsecureUnused = E.New("tls: insecure unused")
type STDServerConfig struct {
access sync.RWMutex
config *tls.Config
logger log.Logger
acmeService adapter.SimpleLifecycle
@@ -33,14 +35,22 @@ type STDServerConfig struct {
}
func (c *STDServerConfig) ServerName() string {
c.access.RLock()
defer c.access.RUnlock()
return c.config.ServerName
}
func (c *STDServerConfig) SetServerName(serverName string) {
c.config.ServerName = serverName
c.access.Lock()
defer c.access.Unlock()
config := c.config.Clone()
config.ServerName = serverName
c.config = config
}
func (c *STDServerConfig) NextProtos() []string {
c.access.RLock()
defer c.access.RUnlock()
if c.acmeService != nil && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == ACMETLS1Protocol {
return c.config.NextProtos[1:]
} else {
@@ -49,11 +59,15 @@ func (c *STDServerConfig) NextProtos() []string {
}
func (c *STDServerConfig) SetNextProtos(nextProto []string) {
c.access.Lock()
defer c.access.Unlock()
config := c.config.Clone()
if c.acmeService != nil && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == ACMETLS1Protocol {
c.config.NextProtos = append(c.config.NextProtos[:1], nextProto...)
config.NextProtos = append(c.config.NextProtos[:1], nextProto...)
} else {
c.config.NextProtos = nextProto
config.NextProtos = nextProto
}
c.config = config
}
func (c *STDServerConfig) STDConfig() (*STDConfig, error) {
@@ -78,9 +92,6 @@ func (c *STDServerConfig) Start() error {
if c.acmeService != nil {
return c.acmeService.Start()
} else {
if c.certificatePath == "" && c.keyPath == "" {
return nil
}
err := c.startWatcher()
if err != nil {
c.logger.Warn("create fsnotify watcher: ", err)
@@ -100,6 +111,9 @@ func (c *STDServerConfig) startWatcher() error {
if c.echKeyPath != "" {
watchPath = append(watchPath, c.echKeyPath)
}
if len(watchPath) == 0 {
return nil
}
watcher, err := fswatch.NewWatcher(fswatch.Options{
Path: watchPath,
Callback: func(path string) {
@@ -139,10 +153,18 @@ func (c *STDServerConfig) certificateUpdated(path string) error {
if err != nil {
return E.Cause(err, "reload key pair")
}
c.config.Certificates = []tls.Certificate{keyPair}
c.access.Lock()
config := c.config.Clone()
config.Certificates = []tls.Certificate{keyPair}
c.config = config
c.access.Unlock()
c.logger.Info("reloaded TLS certificate")
} else if path == c.echKeyPath {
err := reloadECHKeys(c.echKeyPath, c.config)
echKey, err := os.ReadFile(c.echKeyPath)
if err != nil {
return E.Cause(err, "reload ECH keys from ", c.echKeyPath)
}
err = c.setECHServerConfig(echKey)
if err != nil {
return err
}
@@ -263,7 +285,7 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.
return nil, err
}
}
var config ServerConfig = &STDServerConfig{
serverConfig := &STDServerConfig{
config: tlsConfig,
logger: logger,
acmeService: acmeService,
@@ -273,6 +295,12 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.
keyPath: options.KeyPath,
echKeyPath: echKeyPath,
}
serverConfig.config.GetConfigForClient = func(info *tls.ClientHelloInfo) (*tls.Config, error) {
serverConfig.access.Lock()
defer serverConfig.access.Unlock()
return serverConfig.config, nil
}
var config ServerConfig = serverConfig
if options.KernelTx || options.KernelRx {
if !C.IsLinux {
return nil, E.New("kTLS is only supported on Linux")

View File

@@ -46,15 +46,15 @@ func (s *HistoryStorage) LoadURLTestHistory(tag string) *adapter.URLTestHistory
func (s *HistoryStorage) DeleteURLTestHistory(tag string) {
s.access.Lock()
delete(s.delayHistory, tag)
s.access.Unlock()
s.notifyUpdated()
s.access.Unlock()
}
func (s *HistoryStorage) StoreURLTestHistory(tag string, history *adapter.URLTestHistory) {
s.access.Lock()
s.delayHistory[tag] = history
s.access.Unlock()
s.notifyUpdated()
s.access.Unlock()
}
func (s *HistoryStorage) notifyUpdated() {
@@ -68,6 +68,8 @@ func (s *HistoryStorage) notifyUpdated() {
}
func (s *HistoryStorage) Close() error {
s.access.Lock()
defer s.access.Unlock()
s.updateHook = nil
return nil
}

View File

@@ -214,6 +214,7 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
response.Answer = append(response.Answer, validResponse.Answer...)
}
}*/
disableCache = disableCache || response.Rcode != dns.RcodeSuccess || len(response.Answer) == 0
if responseChecker != nil {
var rejected bool
// TODO: add accept_any rule and support to check response instead of addresses
@@ -280,7 +281,7 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
}
}
logExchangedResponse(c.logger, ctx, response, timeToLive)
return response, err
return response, nil
}
func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error) {

View File

@@ -5,6 +5,7 @@ import (
)
const (
RcodeSuccess RcodeError = mDNS.RcodeSuccess
RcodeFormatError RcodeError = mDNS.RcodeFormatError
RcodeNameError RcodeError = mDNS.RcodeNameError
RcodeRefused RcodeError = mDNS.RcodeRefused

View File

@@ -2,10 +2,13 @@ package dhcp
import (
"context"
"errors"
"io"
"net"
"runtime"
"strings"
"sync"
"syscall"
"time"
"github.com/sagernet/sing-box/adapter"
@@ -195,7 +198,17 @@ func (t *Transport) fetchServers0(ctx context.Context, iface *control.Interface)
if runtime.GOOS == "linux" || runtime.GOOS == "android" {
listenAddr = "255.255.255.255:68"
}
packetConn, err := listener.ListenPacket(t.ctx, "udp4", listenAddr)
var (
packetConn net.PacketConn
err error
)
for i := 0; i < 5; i++ {
packetConn, err = listener.ListenPacket(t.ctx, "udp4", listenAddr)
if err == nil || !errors.Is(err, syscall.EADDRINUSE) {
break
}
time.Sleep(time.Second)
}
if err != nil {
return err
}
@@ -232,6 +245,9 @@ func (t *Transport) fetchServersResponse(iface *control.Interface, packetConn ne
for {
_, _, err := buffer.ReadPacketFrom(packetConn)
if err != nil {
if errors.Is(err, io.ErrShortBuffer) {
continue
}
return err
}

View File

@@ -2,12 +2,13 @@ package dhcp
import (
"context"
"errors"
"math/rand"
"strings"
"time"
"syscall"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/dns/transport"
"github.com/sagernet/sing/common/buf"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
@@ -43,7 +44,7 @@ func (t *Transport) exchangeParallel(ctx context.Context, servers []M.Socksaddr,
if response.Rcode != mDNS.RcodeSuccess {
err = dns.RcodeError(response.Rcode)
} else if len(dns.MessageToAddresses(response)) == 0 {
err = E.New(fqdn, ": empty result")
err = dns.RcodeSuccess
}
}
select {
@@ -83,7 +84,7 @@ func (t *Transport) tryOneName(ctx context.Context, servers []M.Socksaddr, fqdn
server := servers[j]
question := message.Question[0]
question.Name = fqdn
response, err := t.exchangeOne(ctx, server, question, C.DNSTimeout, false, true)
response, err := t.exchangeOne(ctx, server, question)
if err != nil {
lastErr = err
continue
@@ -94,62 +95,77 @@ func (t *Transport) tryOneName(ctx context.Context, servers []M.Socksaddr, fqdn
return nil, E.Cause(lastErr, fqdn)
}
func (t *Transport) exchangeOne(ctx context.Context, server M.Socksaddr, question mDNS.Question, timeout time.Duration, useTCP, ad bool) (*mDNS.Msg, error) {
func (t *Transport) exchangeOne(ctx context.Context, server M.Socksaddr, question mDNS.Question) (*mDNS.Msg, error) {
if server.Port == 0 {
server.Port = 53
}
var networks []string
if useTCP {
networks = []string{N.NetworkTCP}
} else {
networks = []string{N.NetworkUDP, N.NetworkTCP}
}
request := &mDNS.Msg{
MsgHdr: mDNS.MsgHdr{
Id: uint16(rand.Uint32()),
RecursionDesired: true,
AuthenticatedData: ad,
AuthenticatedData: true,
},
Question: []mDNS.Question{question},
Compress: true,
}
request.SetEdns0(buf.UDPBufferSize, false)
buffer := buf.Get(buf.UDPBufferSize)
defer buf.Put(buffer)
for _, network := range networks {
ctx, cancel := context.WithDeadline(ctx, time.Now().Add(timeout))
defer cancel()
conn, err := t.dialer.DialContext(ctx, network, server)
if err != nil {
return nil, err
}
defer conn.Close()
if deadline, loaded := ctx.Deadline(); loaded && !deadline.IsZero() {
conn.SetDeadline(deadline)
}
rawMessage, err := request.PackBuffer(buffer)
if err != nil {
return nil, E.Cause(err, "pack request")
}
_, err = conn.Write(rawMessage)
if err != nil {
return nil, E.Cause(err, "write request")
}
n, err := conn.Read(buffer)
if err != nil {
return nil, E.Cause(err, "read response")
}
var response mDNS.Msg
err = response.Unpack(buffer[:n])
if err != nil {
return nil, E.Cause(err, "unpack response")
}
if response.Truncated && network == N.NetworkUDP {
continue
}
return &response, nil
return t.exchangeUDP(ctx, server, request)
}
func (t *Transport) exchangeUDP(ctx context.Context, server M.Socksaddr, request *mDNS.Msg) (*mDNS.Msg, error) {
conn, err := t.dialer.DialContext(ctx, N.NetworkUDP, server)
if err != nil {
return nil, err
}
panic("unexpected")
defer conn.Close()
if deadline, loaded := ctx.Deadline(); loaded && !deadline.IsZero() {
conn.SetDeadline(deadline)
}
buffer := buf.Get(1 + request.Len())
defer buf.Put(buffer)
rawMessage, err := request.PackBuffer(buffer)
if err != nil {
return nil, E.Cause(err, "pack request")
}
_, err = conn.Write(rawMessage)
if err != nil {
if errors.Is(err, syscall.EMSGSIZE) {
return t.exchangeTCP(ctx, server, request)
}
return nil, E.Cause(err, "write request")
}
n, err := conn.Read(buffer)
if err != nil {
if errors.Is(err, syscall.EMSGSIZE) {
return t.exchangeTCP(ctx, server, request)
}
return nil, E.Cause(err, "read response")
}
var response mDNS.Msg
err = response.Unpack(buffer[:n])
if err != nil {
return nil, E.Cause(err, "unpack response")
}
if response.Truncated {
return t.exchangeTCP(ctx, server, request)
}
return &response, nil
}
func (t *Transport) exchangeTCP(ctx context.Context, server M.Socksaddr, request *mDNS.Msg) (*mDNS.Msg, error) {
conn, err := t.dialer.DialContext(ctx, N.NetworkTCP, server)
if err != nil {
return nil, err
}
defer conn.Close()
if deadline, loaded := ctx.Deadline(); loaded && !deadline.IsZero() {
conn.SetDeadline(deadline)
}
err = transport.WriteMessage(conn, 0, request)
if err != nil {
return nil, err
}
return transport.ReadMessage(conn)
}
func (t *Transport) nameList(name string) []string {

View File

@@ -2,11 +2,13 @@ package local
import (
"context"
"fmt"
"errors"
"math/rand"
"syscall"
"time"
"github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/dns/transport"
"github.com/sagernet/sing/common/buf"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
@@ -17,7 +19,6 @@ import (
func (t *Transport) exchange(ctx context.Context, message *mDNS.Msg, domain string) (*mDNS.Msg, error) {
systemConfig := getSystemDNSConfig(t.ctx)
fmt.Println(systemConfig.servers)
if systemConfig.singleRequest || !(message.Question[0].Qtype == mDNS.TypeA || message.Question[0].Qtype == mDNS.TypeAAAA) {
return t.exchangeSingleRequest(ctx, systemConfig, message, domain)
} else {
@@ -108,12 +109,6 @@ func (t *Transport) exchangeOne(ctx context.Context, server M.Socksaddr, questio
if server.Port == 0 {
server.Port = 53
}
var networks []string
if useTCP {
networks = []string{N.NetworkTCP}
} else {
networks = []string{N.NetworkUDP, N.NetworkTCP}
}
request := &mDNS.Msg{
MsgHdr: mDNS.MsgHdr{
Id: uint16(rand.Uint32()),
@@ -124,40 +119,73 @@ func (t *Transport) exchangeOne(ctx context.Context, server M.Socksaddr, questio
Compress: true,
}
request.SetEdns0(buf.UDPBufferSize, false)
buffer := buf.Get(buf.UDPBufferSize)
defer buf.Put(buffer)
for _, network := range networks {
ctx, cancel := context.WithDeadline(ctx, time.Now().Add(timeout))
defer cancel()
conn, err := t.dialer.DialContext(ctx, network, server)
if err != nil {
return nil, err
}
defer conn.Close()
if deadline, loaded := ctx.Deadline(); loaded && !deadline.IsZero() {
conn.SetDeadline(deadline)
}
rawMessage, err := request.PackBuffer(buffer)
if err != nil {
return nil, E.Cause(err, "pack request")
}
_, err = conn.Write(rawMessage)
if err != nil {
return nil, E.Cause(err, "write request")
}
n, err := conn.Read(buffer)
if err != nil {
return nil, E.Cause(err, "read response")
}
var response mDNS.Msg
err = response.Unpack(buffer[:n])
if err != nil {
return nil, E.Cause(err, "unpack response")
}
if response.Truncated && network == N.NetworkUDP {
continue
}
return &response, nil
if !useTCP {
return t.exchangeUDP(ctx, server, request, timeout)
} else {
return t.exchangeTCP(ctx, server, request, timeout)
}
panic("unexpected")
}
func (t *Transport) exchangeUDP(ctx context.Context, server M.Socksaddr, request *mDNS.Msg, timeout time.Duration) (*mDNS.Msg, error) {
conn, err := t.dialer.DialContext(ctx, N.NetworkUDP, server)
if err != nil {
return nil, err
}
defer conn.Close()
if deadline, loaded := ctx.Deadline(); loaded && !deadline.IsZero() {
newDeadline := time.Now().Add(timeout)
if deadline.After(newDeadline) {
deadline = newDeadline
}
conn.SetDeadline(deadline)
}
buffer := buf.Get(1 + request.Len())
defer buf.Put(buffer)
rawMessage, err := request.PackBuffer(buffer)
if err != nil {
return nil, E.Cause(err, "pack request")
}
_, err = conn.Write(rawMessage)
if err != nil {
if errors.Is(err, syscall.EMSGSIZE) {
return t.exchangeTCP(ctx, server, request, timeout)
}
return nil, E.Cause(err, "write request")
}
n, err := conn.Read(buffer)
if err != nil {
if errors.Is(err, syscall.EMSGSIZE) {
return t.exchangeTCP(ctx, server, request, timeout)
}
return nil, E.Cause(err, "read response")
}
var response mDNS.Msg
err = response.Unpack(buffer[:n])
if err != nil {
return nil, E.Cause(err, "unpack response")
}
if response.Truncated {
return t.exchangeTCP(ctx, server, request, timeout)
}
return &response, nil
}
func (t *Transport) exchangeTCP(ctx context.Context, server M.Socksaddr, request *mDNS.Msg, timeout time.Duration) (*mDNS.Msg, error) {
conn, err := t.dialer.DialContext(ctx, N.NetworkTCP, server)
if err != nil {
return nil, err
}
defer conn.Close()
if deadline, loaded := ctx.Deadline(); loaded && !deadline.IsZero() {
newDeadline := time.Now().Add(timeout)
if deadline.After(newDeadline) {
deadline = newDeadline
}
conn.SetDeadline(deadline)
}
err = transport.WriteMessage(conn, 0, request)
if err != nil {
return nil, err
}
return transport.ReadMessage(conn)
}

View File

@@ -2,6 +2,14 @@
icon: material/alert-decagram
---
#### 1.13.0-alpha.13
* Fixes and improvements
#### 1.12.7
* Fixes and improvements
#### 1.13.0-alpha.11
* Fixes and improvements

2
go.mod
View File

@@ -33,7 +33,7 @@ require (
github.com/sagernet/sing-shadowsocks v0.2.8
github.com/sagernet/sing-shadowsocks2 v0.2.1
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11
github.com/sagernet/sing-tun v0.8.0-beta.8
github.com/sagernet/sing-tun v0.8.0-beta.8.0.20250911105100-0381a06643bc
github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1
github.com/sagernet/smux v1.5.34-mod.2
github.com/sagernet/tailscale v1.80.3-sing-box-1.13-mod.1

4
go.sum
View File

@@ -179,8 +179,8 @@ github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnq
github.com/sagernet/sing-shadowsocks2 v0.2.1/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ=
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 h1:tK+75l64tm9WvEFrYRE1t0YxoFdWQqw/h7Uhzj0vJ+w=
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA=
github.com/sagernet/sing-tun v0.8.0-beta.8 h1:K/tPAiuW4Qf1usZvdqjAMVwaxAVE/fb64YqO9SCvcTg=
github.com/sagernet/sing-tun v0.8.0-beta.8/go.mod h1:DCGwHe70ujuzmQ3bvUnf9u1FFRoRvBQ1dDpqZov1ZDA=
github.com/sagernet/sing-tun v0.8.0-beta.8.0.20250911105100-0381a06643bc h1:u6MwQ35W6gIVWRAVjKx3TtaNIhO39Bsz5LkvkWGnnuk=
github.com/sagernet/sing-tun v0.8.0-beta.8.0.20250911105100-0381a06643bc/go.mod h1:DCGwHe70ujuzmQ3bvUnf9u1FFRoRvBQ1dDpqZov1ZDA=
github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 h1:aSwUNYUkVyVvdmBSufR8/nRFonwJeKSIROxHcm5br9o=
github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1/go.mod h1:P11scgTxMxVVQ8dlM27yNm3Cro40mD0+gHbnqrNGDuY=
github.com/sagernet/smux v1.5.34-mod.2 h1:gkmBjIjlJ2zQKpLigOkFur5kBKdV6bNRoFu2WkltRQ4=

View File

@@ -14,7 +14,7 @@ import (
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
tun "github.com/sagernet/sing-tun"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/batch"
E "github.com/sagernet/sing/common/exceptions"

View File

@@ -17,6 +17,7 @@ import (
R "github.com/sagernet/sing-box/route/rule"
"github.com/sagernet/sing-mux"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing-tun/ping"
"github.com/sagernet/sing-vmess"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/buf"
@@ -271,6 +272,7 @@ func (r *Router) PreMatch(metadata adapter.InboundContext, routeContext tun.Dire
if err != nil {
return nil, err
}
var directRouteOutbound adapter.DirectRouteOutbound
if selectedRule != nil {
switch action := selectedRule.Action().(type) {
case *R.RuleActionReject:
@@ -296,17 +298,69 @@ func (r *Router) PreMatch(metadata adapter.InboundContext, routeContext tun.Dire
if !common.Contains(outbound.Network(), metadata.Network) {
return nil, E.New(metadata.Network, " is not supported by outbound: ", action.Outbound)
}
return outbound.(adapter.DirectRouteOutbound).NewDirectRouteConnection(metadata, routeContext, timeout)
directRouteOutbound = outbound.(adapter.DirectRouteOutbound)
}
}
if selectedRule != nil || metadata.Network != N.NetworkICMP {
return nil, nil
if directRouteOutbound == nil {
if selectedRule != nil || metadata.Network != N.NetworkICMP {
return nil, nil
}
defaultOutbound := r.outbound.Default()
if !common.Contains(defaultOutbound.Network(), metadata.Network) {
return nil, E.New(metadata.Network, " is not supported by default outbound: ", defaultOutbound.Tag())
}
directRouteOutbound = defaultOutbound.(adapter.DirectRouteOutbound)
}
defaultOutbound := r.outbound.Default()
if !common.Contains(defaultOutbound.Network(), metadata.Network) {
return nil, E.New(metadata.Network, " is not supported by default outbound: ", defaultOutbound.Tag())
if metadata.Destination.IsFqdn() {
if len(metadata.DestinationAddresses) == 0 {
var strategy C.DomainStrategy
if metadata.Source.IsIPv4() {
strategy = C.DomainStrategyIPv4Only
} else {
strategy = C.DomainStrategyIPv6Only
}
err = r.actionResolve(r.ctx, &metadata, &R.RuleActionResolve{
Strategy: strategy,
})
if err != nil {
return nil, err
}
}
var newDestination netip.Addr
if metadata.Source.IsIPv4() {
for _, address := range metadata.DestinationAddresses {
if address.Is4() {
newDestination = address
break
}
}
} else {
for _, address := range metadata.DestinationAddresses {
if address.Is6() {
newDestination = address
break
}
}
}
if !newDestination.IsValid() {
if metadata.Source.IsIPv4() {
return nil, E.New("no IPv4 address found for domain: ", metadata.Destination.Fqdn)
} else {
return nil, E.New("no IPv6 address found for domain: ", metadata.Destination.Fqdn)
}
}
metadata.Destination = M.Socksaddr{
Addr: newDestination,
}
routeContext = ping.NewContextDestinationWriter(routeContext, metadata.OriginDestination.Addr)
var routeDestination tun.DirectRouteDestination
routeDestination, err = directRouteOutbound.NewDirectRouteConnection(metadata, routeContext, timeout)
if err != nil {
return nil, err
}
return ping.NewDestinationWriter(routeDestination, newDestination), nil
}
return defaultOutbound.(adapter.DirectRouteOutbound).NewDirectRouteConnection(metadata, routeContext, timeout)
return directRouteOutbound.NewDirectRouteConnection(metadata, routeContext, timeout)
}
func (r *Router) matchRule(

View File

@@ -27,16 +27,16 @@ import (
var _ adapter.RuleSet = (*LocalRuleSet)(nil)
type LocalRuleSet struct {
ctx context.Context
logger logger.Logger
tag string
rules []adapter.HeadlessRule
metadata adapter.RuleSetMetadata
fileFormat string
watcher *fswatch.Watcher
callbackAccess sync.Mutex
callbacks list.List[adapter.RuleSetUpdateCallback]
refs atomic.Int32
ctx context.Context
logger logger.Logger
tag string
access sync.RWMutex
rules []adapter.HeadlessRule
metadata adapter.RuleSetMetadata
fileFormat string
watcher *fswatch.Watcher
callbacks list.List[adapter.RuleSetUpdateCallback]
refs atomic.Int32
}
func NewLocalRuleSet(ctx context.Context, logger logger.Logger, options option.RuleSet) (*LocalRuleSet, error) {
@@ -141,11 +141,11 @@ func (s *LocalRuleSet) reloadRules(headlessRules []option.HeadlessRule) error {
metadata.ContainsProcessRule = HasHeadlessRule(headlessRules, isProcessHeadlessRule)
metadata.ContainsWIFIRule = HasHeadlessRule(headlessRules, isWIFIHeadlessRule)
metadata.ContainsIPCIDRRule = HasHeadlessRule(headlessRules, isIPCIDRHeadlessRule)
s.access.Lock()
s.rules = rules
s.metadata = metadata
s.callbackAccess.Lock()
callbacks := s.callbacks.Array()
s.callbackAccess.Unlock()
s.access.Unlock()
for _, callback := range callbacks {
callback(s)
}
@@ -157,10 +157,14 @@ func (s *LocalRuleSet) PostStart() error {
}
func (s *LocalRuleSet) Metadata() adapter.RuleSetMetadata {
s.access.RLock()
defer s.access.RUnlock()
return s.metadata
}
func (s *LocalRuleSet) ExtractIPSet() []*netipx.IPSet {
s.access.RLock()
defer s.access.RUnlock()
return common.FlatMap(s.rules, extractIPSetFromRule)
}
@@ -181,14 +185,14 @@ func (s *LocalRuleSet) Cleanup() {
}
func (s *LocalRuleSet) RegisterCallback(callback adapter.RuleSetUpdateCallback) *list.Element[adapter.RuleSetUpdateCallback] {
s.callbackAccess.Lock()
defer s.callbackAccess.Unlock()
s.access.Lock()
defer s.access.Unlock()
return s.callbacks.PushBack(callback)
}
func (s *LocalRuleSet) UnregisterCallback(element *list.Element[adapter.RuleSetUpdateCallback]) {
s.callbackAccess.Lock()
defer s.callbackAccess.Unlock()
s.access.Lock()
defer s.access.Unlock()
s.callbacks.Remove(element)
}

View File

@@ -40,16 +40,16 @@ type RemoteRuleSet struct {
logger logger.ContextLogger
outbound adapter.OutboundManager
options option.RuleSet
metadata adapter.RuleSetMetadata
updateInterval time.Duration
dialer N.Dialer
access sync.RWMutex
rules []adapter.HeadlessRule
metadata adapter.RuleSetMetadata
lastUpdated time.Time
lastEtag string
updateTicker *time.Ticker
cacheFile adapter.CacheFile
pauseManager pause.Manager
callbackAccess sync.Mutex
callbacks list.List[adapter.RuleSetUpdateCallback]
refs atomic.Int32
}
@@ -120,10 +120,14 @@ func (s *RemoteRuleSet) PostStart() error {
}
func (s *RemoteRuleSet) Metadata() adapter.RuleSetMetadata {
s.access.RLock()
defer s.access.RUnlock()
return s.metadata
}
func (s *RemoteRuleSet) ExtractIPSet() []*netipx.IPSet {
s.access.RLock()
defer s.access.RUnlock()
return common.FlatMap(s.rules, extractIPSetFromRule)
}
@@ -144,14 +148,14 @@ func (s *RemoteRuleSet) Cleanup() {
}
func (s *RemoteRuleSet) RegisterCallback(callback adapter.RuleSetUpdateCallback) *list.Element[adapter.RuleSetUpdateCallback] {
s.callbackAccess.Lock()
defer s.callbackAccess.Unlock()
s.access.Lock()
defer s.access.Unlock()
return s.callbacks.PushBack(callback)
}
func (s *RemoteRuleSet) UnregisterCallback(element *list.Element[adapter.RuleSetUpdateCallback]) {
s.callbackAccess.Lock()
defer s.callbackAccess.Unlock()
s.access.Lock()
defer s.access.Unlock()
s.callbacks.Remove(element)
}
@@ -185,13 +189,13 @@ func (s *RemoteRuleSet) loadBytes(content []byte) error {
return E.Cause(err, "parse rule_set.rules.[", i, "]")
}
}
s.access.Lock()
s.metadata.ContainsProcessRule = HasHeadlessRule(plainRuleSet.Rules, isProcessHeadlessRule)
s.metadata.ContainsWIFIRule = HasHeadlessRule(plainRuleSet.Rules, isWIFIHeadlessRule)
s.metadata.ContainsIPCIDRRule = HasHeadlessRule(plainRuleSet.Rules, isIPCIDRHeadlessRule)
s.rules = rules
s.callbackAccess.Lock()
callbacks := s.callbacks.Array()
s.callbackAccess.Unlock()
s.access.Unlock()
for _, callback := range callbacks {
callback(s)
}

View File

@@ -20,7 +20,7 @@ type natDeviceWrapper struct {
ctx context.Context
logger logger.ContextLogger
packetOutbound chan *buf.Buffer
rewriter *ping.Rewriter
rewriter *ping.SourceRewriter
buffer [][]byte
}
@@ -30,7 +30,7 @@ func NewNATDevice(ctx context.Context, logger logger.ContextLogger, upstream Dev
ctx: ctx,
logger: logger,
packetOutbound: make(chan *buf.Buffer, 256),
rewriter: ping.NewRewriter(ctx, logger, upstream.Inet4Address(), upstream.Inet6Address()),
rewriter: ping.NewSourceRewriter(ctx, logger, upstream.Inet4Address(), upstream.Inet6Address()),
}
return wrapper
}