Compare commits

...

16 Commits

Author SHA1 Message Date
世界
06ee404918
documentation: Bump version 2024-07-07 16:10:24 +08:00
世界
687e631f3d
Add inline rule-set & Add reload for local rule-set 2024-07-07 16:10:24 +08:00
世界
a276121013
Unique rule-set names 2024-07-07 16:10:24 +08:00
世界
285336261b
Add accept empty DNS rule option 2024-07-07 16:10:23 +08:00
世界
8a11417dd5
Add custom options for TUN auto-route and auto-redirect 2024-07-07 16:10:23 +08:00
世界
53e5aa64b4
Improve base DNS transports 2024-07-07 16:10:23 +08:00
世界
97ef3ca0f3
Add auto-redirect & Improve auto-route 2024-07-07 16:10:23 +08:00
世界
ba2eb6e49f
Add rule-set decompile command 2024-07-07 16:10:23 +08:00
世界
df331ba0c3
Add IP address support for rule-set match match 2024-07-07 16:10:23 +08:00
世界
3f67845d4f
WTF is this 2024-07-07 16:10:23 +08:00
世界
dedeba0e6d
platform: Fix clash server reload on android 2024-07-07 16:10:22 +08:00
世界
4f870150fc
platform: Add log update interval 2024-07-07 16:10:22 +08:00
世界
85aaa112c4
platform: Prepare connections list 2024-07-07 16:10:22 +08:00
世界
19f6c6abfd
Drop support for go1.18 and go1.19 2024-07-07 16:10:22 +08:00
世界
3677e7ab9e
Introduce DTLS sniffer 2024-07-07 16:10:22 +08:00
iosmanthus
2d2e174a6e
Introduce bittorrent related protocol sniffers
* Introduce bittorrent related protocol sniffers

including, sniffers of
1. BitTorrent Protocol (TCP)
2. uTorrent Transport Protocol (UDP)

Signed-off-by: iosmanthus <myosmanthustree@gmail.com>
Co-authored-by: 世界 <i@sekai.icu>
2024-07-07 16:10:22 +08:00
103 changed files with 3343 additions and 1339 deletions

View File

@ -33,26 +33,6 @@ jobs:
- name: Run Test - name: Run Test
run: | run: |
go test -v ./... go test -v ./...
build_go118:
name: Debug build (Go 1.18)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ~1.18
- name: Cache go module
uses: actions/cache@v4
with:
path: |
~/go/pkg/mod
key: go118-${{ hashFiles('**/go.sum') }}
- name: Run Test
run: make ci_build_go118
build_go120: build_go120:
name: Debug build (Go 1.20) name: Debug build (Go 1.20)
runs-on: ubuntu-latest runs-on: ubuntu-latest

View File

@ -1,7 +1,6 @@
NAME = sing-box NAME = sing-box
COMMIT = $(shell git rev-parse --short HEAD) COMMIT = $(shell git rev-parse --short HEAD)
TAGS_GO118 = with_gvisor,with_dhcp,with_wireguard,with_reality_server,with_clash_api TAGS_GO120 = with_gvisor,with_dhcp,with_wireguard,with_reality_server,with_clash_api,with_quic,with_utls
TAGS_GO120 = with_quic,with_utls
TAGS_GO121 = with_ech TAGS_GO121 = with_ech
TAGS ?= $(TAGS_GO118),$(TAGS_GO120),$(TAGS_GO121) TAGS ?= $(TAGS_GO118),$(TAGS_GO120),$(TAGS_GO121)
TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_ech,with_utls,with_reality_server TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_ech,with_utls,with_reality_server
@ -20,13 +19,9 @@ PREFIX ?= $(shell go env GOPATH)
build: build:
go build $(MAIN_PARAMS) $(MAIN) go build $(MAIN_PARAMS) $(MAIN)
ci_build_go118:
go build $(PARAMS) $(MAIN)
go build $(PARAMS) -tags "$(TAGS_GO118)" $(MAIN)
ci_build_go120: ci_build_go120:
go build $(PARAMS) $(MAIN) go build $(PARAMS) $(MAIN)
go build $(PARAMS) -tags "$(TAGS_GO118),$(TAGS_GO120)" $(MAIN) go build $(PARAMS) -tags "$(TAGS_GO120)" $(MAIN)
ci_build: ci_build:
go build $(PARAMS) $(MAIN) go build $(PARAMS) $(MAIN)

View File

@ -4,14 +4,13 @@ import (
"bytes" "bytes"
"context" "context"
"encoding/binary" "encoding/binary"
"io"
"net" "net"
"time" "time"
"github.com/sagernet/sing-box/common/urltest" "github.com/sagernet/sing-box/common/urltest"
"github.com/sagernet/sing-dns" "github.com/sagernet/sing-dns"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/rw" "github.com/sagernet/sing/common/varbin"
) )
type ClashServer interface { type ClashServer interface {
@ -56,16 +55,15 @@ func (s *SavedRuleSet) MarshalBinary() ([]byte, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
err = rw.WriteUVariant(&buffer, uint64(len(s.Content))) err = varbin.Write(&buffer, binary.BigEndian, s.Content)
if err != nil { if err != nil {
return nil, err return nil, err
} }
buffer.Write(s.Content)
err = binary.Write(&buffer, binary.BigEndian, s.LastUpdated.Unix()) err = binary.Write(&buffer, binary.BigEndian, s.LastUpdated.Unix())
if err != nil { if err != nil {
return nil, err return nil, err
} }
err = rw.WriteVString(&buffer, s.LastEtag) err = varbin.Write(&buffer, binary.BigEndian, s.LastEtag)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -79,12 +77,7 @@ func (s *SavedRuleSet) UnmarshalBinary(data []byte) error {
if err != nil { if err != nil {
return err return err
} }
contentLen, err := rw.ReadUVariant(reader) err = varbin.Read(reader, binary.BigEndian, &s.Content)
if err != nil {
return err
}
s.Content = make([]byte, contentLen)
_, err = io.ReadFull(reader, s.Content)
if err != nil { if err != nil {
return err return err
} }
@ -94,7 +87,7 @@ func (s *SavedRuleSet) UnmarshalBinary(data []byte) error {
return err return err
} }
s.LastUpdated = time.Unix(lastUpdated, 0) s.LastUpdated = time.Unix(lastUpdated, 0)
s.LastEtag, err = rw.ReadVString(reader) err = varbin.Read(reader, binary.BigEndian, &s.LastEtag)
if err != nil { if err != nil {
return err return err
} }

View File

@ -51,7 +51,9 @@ type InboundContext struct {
// rule cache // rule cache
IPCIDRMatchSource bool IPCIDRMatchSource bool
IPCIDRAcceptEmpty bool
SourceAddressMatch bool SourceAddressMatch bool
SourcePortMatch bool SourcePortMatch bool
DestinationAddressMatch bool DestinationAddressMatch bool
@ -62,6 +64,7 @@ type InboundContext struct {
func (c *InboundContext) ResetRuleCache() { func (c *InboundContext) ResetRuleCache() {
c.IPCIDRMatchSource = false c.IPCIDRMatchSource = false
c.IPCIDRAcceptEmpty = false
c.SourceAddressMatch = false c.SourceAddressMatch = false
c.SourcePortMatch = false c.SourcePortMatch = false
c.DestinationAddressMatch = false c.DestinationAddressMatch = false

View File

@ -10,15 +10,18 @@ import (
"github.com/sagernet/sing-tun" "github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common/control" "github.com/sagernet/sing/common/control"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/x/list"
"github.com/sagernet/sing/service" "github.com/sagernet/sing/service"
mdns "github.com/miekg/dns" mdns "github.com/miekg/dns"
"go4.org/netipx"
) )
type Router interface { type Router interface {
Service Service
PreStarter PreStarter
PostStarter PostStarter
Cleanup() error
Outbounds() []Outbound Outbounds() []Outbound
Outbound(tag string) (Outbound, bool) Outbound(tag string) (Outbound, bool)
@ -45,7 +48,9 @@ type Router interface {
DefaultInterface() string DefaultInterface() string
AutoDetectInterface() bool AutoDetectInterface() bool
AutoDetectInterfaceFunc() control.Func AutoDetectInterfaceFunc() control.Func
DefaultMark() int DefaultMark() uint32
RegisterAutoRedirectOutputMark(mark uint32) error
AutoRedirectOutputMark() uint32
NetworkMonitor() tun.NetworkUpdateMonitor NetworkMonitor() tun.NetworkUpdateMonitor
InterfaceMonitor() tun.DefaultInterfaceMonitor InterfaceMonitor() tun.DefaultInterfaceMonitor
PackageManager() tun.PackageManager PackageManager() tun.PackageManager
@ -92,12 +97,22 @@ type DNSRule interface {
} }
type RuleSet interface { type RuleSet interface {
Name() string
StartContext(ctx context.Context, startContext RuleSetStartContext) error StartContext(ctx context.Context, startContext RuleSetStartContext) error
PostStart() error
Metadata() RuleSetMetadata Metadata() RuleSetMetadata
ExtractIPSet() []*netipx.IPSet
IncRef()
DecRef()
Cleanup()
RegisterCallback(callback RuleSetUpdateCallback) *list.Element[RuleSetUpdateCallback]
UnregisterCallback(element *list.Element[RuleSetUpdateCallback])
Close() error Close() error
HeadlessRule HeadlessRule
} }
type RuleSetUpdateCallback func(it RuleSet)
type RuleSetMetadata struct { type RuleSetMetadata struct {
ContainsProcessRule bool ContainsProcessRule bool
ContainsWIFIRule bool ContainsWIFIRule bool

29
box.go
View File

@ -111,6 +111,7 @@ func New(options Options) (*Box, error) {
ctx, ctx,
router, router,
logFactory.NewLogger(F.ToString("inbound/", inboundOptions.Type, "[", tag, "]")), logFactory.NewLogger(F.ToString("inbound/", inboundOptions.Type, "[", tag, "]")),
tag,
inboundOptions, inboundOptions,
options.PlatformInterface, options.PlatformInterface,
) )
@ -302,7 +303,11 @@ func (s *Box) start() error {
return E.Cause(err, "initialize inbound/", in.Type(), "[", tag, "]") return E.Cause(err, "initialize inbound/", in.Type(), "[", tag, "]")
} }
} }
return s.postStart() err = s.postStart()
if err != nil {
return err
}
return s.router.Cleanup()
} }
func (s *Box) postStart() error { func (s *Box) postStart() error {
@ -312,16 +317,28 @@ func (s *Box) postStart() error {
return E.Cause(err, "start ", serviceName) return E.Cause(err, "start ", serviceName)
} }
} }
for _, outbound := range s.outbounds { // TODO: reorganize ALL start order
if lateOutbound, isLateOutbound := outbound.(adapter.PostStarter); isLateOutbound { for _, out := range s.outbounds {
if lateOutbound, isLateOutbound := out.(adapter.PostStarter); isLateOutbound {
err := lateOutbound.PostStart() err := lateOutbound.PostStart()
if err != nil { if err != nil {
return E.Cause(err, "post-start outbound/", outbound.Tag()) return E.Cause(err, "post-start outbound/", out.Tag())
} }
} }
} }
err := s.router.PostStart()
return s.router.PostStart() if err != nil {
return err
}
for _, in := range s.inbounds {
if lateInbound, isLateInbound := in.(adapter.PostStarter); isLateInbound {
err = lateInbound.PostStart()
if err != nil {
return E.Cause(err, "post-start inbound/", in.Tag())
}
}
}
return nil
} }
func (s *Box) Close() error { func (s *Box) Close() error {

View File

@ -45,7 +45,9 @@ func (s *Box) startOutbounds() error {
} }
started[outboundTag] = true started[outboundTag] = true
canContinue = true canContinue = true
if starter, isStarter := outboundToStart.(common.Starter); isStarter { if starter, isStarter := outboundToStart.(interface {
Start() error
}); isStarter {
monitor.Start("initialize outbound/", outboundToStart.Type(), "[", outboundTag, "]") monitor.Start("initialize outbound/", outboundToStart.Type(), "[", outboundTag, "]")
err := starter.Start() err := starter.Start()
monitor.Finish() monitor.Finish()

View File

@ -93,7 +93,7 @@ func buildAndroid() {
const name = "libbox.aar" const name = "libbox.aar"
copyPath := filepath.Join("..", "sing-box-for-android", "app", "libs") copyPath := filepath.Join("..", "sing-box-for-android", "app", "libs")
if rw.FileExists(copyPath) { if rw.IsDir(copyPath) {
copyPath, _ = filepath.Abs(copyPath) copyPath, _ = filepath.Abs(copyPath)
err = rw.CopyFile(name, filepath.Join(copyPath, name)) err = rw.CopyFile(name, filepath.Join(copyPath, name))
if err != nil { if err != nil {
@ -134,7 +134,7 @@ func buildiOS() {
} }
copyPath := filepath.Join("..", "sing-box-for-apple") copyPath := filepath.Join("..", "sing-box-for-apple")
if rw.FileExists(copyPath) { if rw.IsDir(copyPath) {
targetDir := filepath.Join(copyPath, "Libbox.xcframework") targetDir := filepath.Join(copyPath, "Libbox.xcframework")
targetDir, _ = filepath.Abs(targetDir) targetDir, _ = filepath.Abs(targetDir)
os.RemoveAll(targetDir) os.RemoveAll(targetDir)

View File

@ -30,7 +30,7 @@ func FindSDK() {
} }
for _, path := range searchPath { for _, path := range searchPath {
path = os.ExpandEnv(path) path = os.ExpandEnv(path)
if rw.FileExists(filepath.Join(path, "licenses", "android-sdk-license")) { if rw.IsFile(filepath.Join(path, "licenses", "android-sdk-license")) {
androidSDKPath = path androidSDKPath = path
break break
} }
@ -60,7 +60,7 @@ func FindSDK() {
func findNDK() bool { func findNDK() bool {
const fixedVersion = "26.2.11394342" const fixedVersion = "26.2.11394342"
const versionFile = "source.properties" const versionFile = "source.properties"
if fixedPath := filepath.Join(androidSDKPath, "ndk", fixedVersion); rw.FileExists(filepath.Join(fixedPath, versionFile)) { if fixedPath := filepath.Join(androidSDKPath, "ndk", fixedVersion); rw.IsFile(filepath.Join(fixedPath, versionFile)) {
androidNDKPath = fixedPath androidNDKPath = fixedPath
return true return true
} }
@ -86,7 +86,7 @@ func findNDK() bool {
}) })
for _, versionName := range versionNames { for _, versionName := range versionNames {
currentNDKPath := filepath.Join(androidSDKPath, "ndk", versionName) currentNDKPath := filepath.Join(androidSDKPath, "ndk", versionName)
if rw.FileExists(filepath.Join(androidSDKPath, versionFile)) { if rw.IsFile(filepath.Join(androidSDKPath, versionFile)) {
androidNDKPath = currentNDKPath androidNDKPath = currentNDKPath
log.Warn("reproducibility warning: using NDK version " + versionName + " instead of " + fixedVersion) log.Warn("reproducibility warning: using NDK version " + versionName + " instead of " + fixedVersion)
return true return true
@ -100,11 +100,11 @@ var GoBinPath string
func FindMobile() { func FindMobile() {
goBin := filepath.Join(build.Default.GOPATH, "bin") goBin := filepath.Join(build.Default.GOPATH, "bin")
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
if !rw.FileExists(filepath.Join(goBin, "gobind.exe")) { if !rw.IsFile(filepath.Join(goBin, "gobind.exe")) {
log.Fatal("missing gomobile installation") log.Fatal("missing gomobile installation")
} }
} else { } else {
if !rw.FileExists(filepath.Join(goBin, "gobind")) { if !rw.IsFile(filepath.Join(goBin, "gobind")) {
log.Fatal("missing gomobile installation") log.Fatal("missing gomobile installation")
} }
} }

View File

@ -54,7 +54,11 @@ func merge(outputPath string) error {
return nil return nil
} }
} }
err = rw.WriteFile(outputPath, buffer.Bytes()) err = rw.MkdirParent(outputPath)
if err != nil {
return err
}
err = os.WriteFile(outputPath, buffer.Bytes(), 0o644)
if err != nil { if err != nil {
return err return err
} }

View File

@ -6,7 +6,7 @@ import (
var commandRuleSet = &cobra.Command{ var commandRuleSet = &cobra.Command{
Use: "rule-set", Use: "rule-set",
Short: "Manage rule sets", Short: "Manage rule-sets",
} }
func init() { func init() {

View File

@ -55,10 +55,10 @@ func compileRuleSet(sourcePath string) error {
if err != nil { if err != nil {
return err return err
} }
ruleSet, err := plainRuleSet.Upgrade()
if err != nil { if err != nil {
return err return err
} }
ruleSet := plainRuleSet.Upgrade()
var outputPath string var outputPath string
if flagRuleSetCompileOutput == flagRuleSetCompileDefaultOutput { if flagRuleSetCompileOutput == flagRuleSetCompileDefaultOutput {
if strings.HasSuffix(sourcePath, ".json") { if strings.HasSuffix(sourcePath, ".json") {

View File

@ -0,0 +1,83 @@
package main
import (
"io"
"os"
"strings"
"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/option"
"github.com/sagernet/sing/common/json"
"github.com/spf13/cobra"
)
var flagRuleSetDecompileOutput string
const flagRuleSetDecompileDefaultOutput = "<file_name>.json"
var commandRuleSetDecompile = &cobra.Command{
Use: "decompile [binary-path]",
Short: "Decompile rule-set binary to json",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
err := decompileRuleSet(args[0])
if err != nil {
log.Fatal(err)
}
},
}
func init() {
commandRuleSet.AddCommand(commandRuleSetDecompile)
commandRuleSetDecompile.Flags().StringVarP(&flagRuleSetDecompileOutput, "output", "o", flagRuleSetDecompileDefaultOutput, "Output file")
}
func decompileRuleSet(sourcePath string) error {
var (
reader io.Reader
err error
)
if sourcePath == "stdin" {
reader = os.Stdin
} else {
reader, err = os.Open(sourcePath)
if err != nil {
return err
}
}
plainRuleSet, err := srs.Read(reader, true)
if err != nil {
return err
}
ruleSet := option.PlainRuleSetCompat{
Version: C.RuleSetVersion1,
Options: plainRuleSet,
}
var outputPath string
if flagRuleSetDecompileOutput == flagRuleSetDecompileDefaultOutput {
if strings.HasSuffix(sourcePath, ".srs") {
outputPath = sourcePath[:len(sourcePath)-4] + ".json"
} else {
outputPath = sourcePath + ".json"
}
} else {
outputPath = flagRuleSetDecompileOutput
}
outputFile, err := os.Create(outputPath)
if err != nil {
return err
}
encoder := json.NewEncoder(outputFile)
encoder.SetIndent("", " ")
err = encoder.Encode(ruleSet)
if err != nil {
outputFile.Close()
os.Remove(outputPath)
return err
}
outputFile.Close()
return nil
}

View File

@ -14,6 +14,7 @@ import (
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format" F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/json"
M "github.com/sagernet/sing/common/metadata"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -21,8 +22,8 @@ import (
var flagRuleSetMatchFormat string var flagRuleSetMatchFormat string
var commandRuleSetMatch = &cobra.Command{ var commandRuleSetMatch = &cobra.Command{
Use: "match <rule-set path> <domain>", Use: "match <rule-set path> <IP address/domain>",
Short: "Check if a domain matches the rule set", Short: "Check if an IP address or a domain matches the rule-set",
Args: cobra.ExactArgs(2), Args: cobra.ExactArgs(2),
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
err := ruleSetMatch(args[0], args[1]) err := ruleSetMatch(args[0], args[1])
@ -62,14 +63,24 @@ func ruleSetMatch(sourcePath string, domain string) error {
if err != nil { if err != nil {
return err return err
} }
plainRuleSet = compat.Upgrade() plainRuleSet, err = compat.Upgrade()
if err != nil {
return err
}
case C.RuleSetFormatBinary: case C.RuleSetFormatBinary:
plainRuleSet, err = srs.Read(bytes.NewReader(content), false) plainRuleSet, err = srs.Read(bytes.NewReader(content), false)
if err != nil { if err != nil {
return err return err
} }
default: default:
return E.New("unknown rule set format: ", flagRuleSetMatchFormat) return E.New("unknown rule-set format: ", flagRuleSetMatchFormat)
}
ipAddress := M.ParseAddr(domain)
var metadata adapter.InboundContext
if ipAddress.IsValid() {
metadata.Destination = M.SocksaddrFrom(ipAddress, 0)
} else {
metadata.Domain = domain
} }
for i, ruleOptions := range plainRuleSet.Rules { for i, ruleOptions := range plainRuleSet.Rules {
var currentRule adapter.HeadlessRule var currentRule adapter.HeadlessRule
@ -77,9 +88,7 @@ func ruleSetMatch(sourcePath string, domain string) error {
if err != nil { if err != nil {
return E.Cause(err, "parse rule_set.rules.[", i, "]") return E.Cause(err, "parse rule_set.rules.[", i, "]")
} }
if currentRule.Match(&adapter.InboundContext{ if currentRule.Match(&metadata) {
Domain: domain,
}) {
println(F.ToString("match rules.[", i, "]: ", currentRule)) println(F.ToString("match rules.[", i, "]: ", currentRule))
} }
} }

View File

@ -109,7 +109,7 @@ func readConfigAndMerge() (option.Options, error) {
} }
var mergedMessage json.RawMessage var mergedMessage json.RawMessage
for _, options := range optionsList { for _, options := range optionsList {
mergedMessage, err = badjson.MergeJSON(options.options.RawMessage, mergedMessage) mergedMessage, err = badjson.MergeJSON(options.options.RawMessage, mergedMessage, false)
if err != nil { if err != nil {
return option.Options{}, E.Cause(err, "merge config at ", options.path) return option.Options{}, E.Cause(err, "merge config at ", options.path)
} }

View File

@ -9,8 +9,10 @@ import (
"net/url" "net/url"
"os" "os"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common/bufio" "github.com/sagernet/sing/common/bufio"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -32,7 +34,10 @@ func init() {
commandTools.AddCommand(commandFetch) commandTools.AddCommand(commandFetch)
} }
var httpClient *http.Client var (
httpClient *http.Client
http3Client *http.Client
)
func fetch(args []string) error { func fetch(args []string) error {
instance, err := createPreStartedClient() instance, err := createPreStartedClient()
@ -53,8 +58,16 @@ func fetch(args []string) error {
}, },
} }
defer httpClient.CloseIdleConnections() defer httpClient.CloseIdleConnections()
if C.WithQUIC {
err = initializeHTTP3Client(instance)
if err != nil {
return err
}
defer http3Client.CloseIdleConnections()
}
for _, urlString := range args { for _, urlString := range args {
parsedURL, err := url.Parse(urlString) var parsedURL *url.URL
parsedURL, err = url.Parse(urlString)
if err != nil { if err != nil {
return err return err
} }
@ -63,16 +76,27 @@ func fetch(args []string) error {
parsedURL.Scheme = "http" parsedURL.Scheme = "http"
fallthrough fallthrough
case "http", "https": case "http", "https":
err = fetchHTTP(parsedURL) err = fetchHTTP(httpClient, parsedURL)
if err != nil { if err != nil {
return err return err
} }
case "http3":
if !C.WithQUIC {
return C.ErrQUICNotIncluded
}
parsedURL.Scheme = "https"
err = fetchHTTP(http3Client, parsedURL)
if err != nil {
return err
}
default:
return E.New("unsupported scheme: ", parsedURL.Scheme)
} }
} }
return nil return nil
} }
func fetchHTTP(parsedURL *url.URL) error { func fetchHTTP(httpClient *http.Client, parsedURL *url.URL) error {
request, err := http.NewRequest("GET", parsedURL.String(), nil) request, err := http.NewRequest("GET", parsedURL.String(), nil)
if err != nil { if err != nil {
return err return err

View File

@ -0,0 +1,36 @@
//go:build with_quic
package main
import (
"context"
"crypto/tls"
"net/http"
"github.com/sagernet/quic-go"
"github.com/sagernet/quic-go/http3"
box "github.com/sagernet/sing-box"
"github.com/sagernet/sing/common/bufio"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
func initializeHTTP3Client(instance *box.Box) error {
dialer, err := createDialer(instance, N.NetworkUDP, commandToolsFlagOutbound)
if err != nil {
return err
}
http3Client = &http.Client{
Transport: &http3.RoundTripper{
Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
destination := M.ParseSocksaddr(addr)
udpConn, dErr := dialer.DialContext(ctx, N.NetworkUDP, destination)
if dErr != nil {
return nil, dErr
}
return quic.DialEarly(ctx, bufio.NewUnbindPacketConn(udpConn), udpConn.RemoteAddr(), tlsCfg, cfg)
},
},
}
return nil
}

View File

@ -0,0 +1,18 @@
//go:build !with_quic
package main
import (
"net/url"
"os"
box "github.com/sagernet/sing-box"
)
func initializeHTTP3Client(instance *box.Box) error {
return os.ErrInvalid
}
func fetchHTTP3(parsedURL *url.URL) error {
return os.ErrInvalid
}

View File

@ -50,12 +50,26 @@ func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDi
dialer.Control = control.Append(dialer.Control, bindFunc) dialer.Control = control.Append(dialer.Control, bindFunc)
listener.Control = control.Append(listener.Control, bindFunc) listener.Control = control.Append(listener.Control, bindFunc)
} }
if options.RoutingMark != 0 { var autoRedirectOutputMark uint32
if router != nil {
autoRedirectOutputMark = router.AutoRedirectOutputMark()
}
if autoRedirectOutputMark > 0 {
dialer.Control = control.Append(dialer.Control, control.RoutingMark(autoRedirectOutputMark))
listener.Control = control.Append(listener.Control, control.RoutingMark(autoRedirectOutputMark))
}
if options.RoutingMark > 0 {
dialer.Control = control.Append(dialer.Control, control.RoutingMark(options.RoutingMark)) dialer.Control = control.Append(dialer.Control, control.RoutingMark(options.RoutingMark))
listener.Control = control.Append(listener.Control, control.RoutingMark(options.RoutingMark)) listener.Control = control.Append(listener.Control, control.RoutingMark(options.RoutingMark))
} else if router != nil && router.DefaultMark() != 0 { if autoRedirectOutputMark > 0 {
return nil, E.New("`auto_redirect` with `route_[_exclude]_address_set is conflict with `routing_mark`")
}
} else if router != nil && router.DefaultMark() > 0 {
dialer.Control = control.Append(dialer.Control, control.RoutingMark(router.DefaultMark())) dialer.Control = control.Append(dialer.Control, control.RoutingMark(router.DefaultMark()))
listener.Control = control.Append(listener.Control, control.RoutingMark(router.DefaultMark())) listener.Control = control.Append(listener.Control, control.RoutingMark(router.DefaultMark()))
if autoRedirectOutputMark > 0 {
return nil, E.New("`auto_redirect` with `route_[_exclude]_address_set is conflict with `default_mark`")
}
} }
if options.ReuseAddr { if options.ReuseAddr {
listener.Control = control.Append(listener.Control, control.ReuseAddr()) listener.Control = control.Append(listener.Control, control.ReuseAddr())

View File

@ -1,11 +1,15 @@
package geosite package geosite
import ( import (
"bufio"
"encoding/binary"
"io" "io"
"os" "os"
"sync/atomic"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/rw" "github.com/sagernet/sing/common/varbin"
) )
type Reader struct { type Reader struct {
@ -34,45 +38,36 @@ func Open(path string) (*Reader, []string, error) {
return reader, codes, nil return reader, codes, nil
} }
type geositeMetadata struct {
Code string
Index uint64
Length uint64
}
func (r *Reader) readMetadata() error { func (r *Reader) readMetadata() error {
version, err := rw.ReadByte(r.reader) reader := bufio.NewReader(r.reader)
version, err := reader.ReadByte()
if err != nil { if err != nil {
return err return err
} }
if version != 0 { if version != 0 {
return E.New("unknown version") return E.New("unknown version")
} }
entryLength, err := rw.ReadUVariant(r.reader) metadataEntries, err := varbin.ReadValue[[]geositeMetadata](reader, binary.BigEndian)
if err != nil { if err != nil {
return err return err
} }
keys := make([]string, entryLength)
domainIndex := make(map[string]int) domainIndex := make(map[string]int)
domainLength := make(map[string]int) domainLength := make(map[string]int)
for i := 0; i < int(entryLength); i++ { for _, entry := range metadataEntries {
var ( domainIndex[entry.Code] = int(entry.Index)
code string domainLength[entry.Code] = int(entry.Length)
codeIndex uint64
codeLength uint64
)
code, err = rw.ReadVString(r.reader)
if err != nil {
return err
}
keys[i] = code
codeIndex, err = rw.ReadUVariant(r.reader)
if err != nil {
return err
}
codeLength, err = rw.ReadUVariant(r.reader)
if err != nil {
return err
}
domainIndex[code] = int(codeIndex)
domainLength[code] = int(codeLength)
} }
r.domainIndex = domainIndex r.domainIndex = domainIndex
r.domainLength = domainLength r.domainLength = domainLength
if reader.Buffered() > 0 {
return common.Error(r.reader.Seek(int64(-reader.Buffered()), io.SeekCurrent))
}
return nil return nil
} }
@ -85,27 +80,28 @@ func (r *Reader) Read(code string) ([]Item, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
counter := &rw.ReadCounter{Reader: r.reader} counter := &readCounter{Reader: r.reader}
domain := make([]Item, r.domainLength[code]) domain, err := varbin.ReadValue[[]Item](bufio.NewReader(counter), binary.BigEndian)
for i := range domain { if err != nil {
var ( return nil, err
item Item
err error
)
item.Type, err = rw.ReadByte(counter)
if err != nil {
return nil, err
}
item.Value, err = rw.ReadVString(counter)
if err != nil {
return nil, err
}
domain[i] = item
} }
_, err = r.reader.Seek(int64(-index)-counter.Count(), io.SeekCurrent) _, err = r.reader.Seek(int64(-index)-counter.count, io.SeekCurrent)
return domain, err return domain, err
} }
func (r *Reader) Upstream() any { func (r *Reader) Upstream() any {
return r.reader return r.reader
} }
type readCounter struct {
io.Reader
count int64
}
func (r *readCounter) Read(p []byte) (n int, err error) {
n, err = r.Reader.Read(p)
if n > 0 {
atomic.AddInt64(&r.count, int64(n))
}
return
}

View File

@ -2,13 +2,14 @@ package geosite
import ( import (
"bytes" "bytes"
"io" "encoding/binary"
"sort" "sort"
"github.com/sagernet/sing/common/rw" "github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/varbin"
) )
func Write(writer io.Writer, domains map[string][]Item) error { func Write(writer varbin.Writer, domains map[string][]Item) error {
keys := make([]string, 0, len(domains)) keys := make([]string, 0, len(domains))
for code := range domains { for code := range domains {
keys = append(keys, code) keys = append(keys, code)
@ -19,40 +20,28 @@ func Write(writer io.Writer, domains map[string][]Item) error {
index := make(map[string]int) index := make(map[string]int)
for _, code := range keys { for _, code := range keys {
index[code] = content.Len() index[code] = content.Len()
for _, domain := range domains[code] { err := varbin.Write(content, binary.BigEndian, domains[code])
content.WriteByte(domain.Type) if err != nil {
err := rw.WriteVString(content, domain.Value) return err
if err != nil {
return err
}
} }
} }
err := rw.WriteByte(writer, 0) err := writer.WriteByte(0)
if err != nil { if err != nil {
return err return err
} }
err = rw.WriteUVariant(writer, uint64(len(keys))) err = varbin.Write(writer, binary.BigEndian, common.Map(keys, func(it string) *geositeMetadata {
return &geositeMetadata{
Code: it,
Index: uint64(index[it]),
Length: uint64(len(domains[it])),
}
}))
if err != nil { if err != nil {
return err return err
} }
for _, code := range keys {
err = rw.WriteVString(writer, code)
if err != nil {
return err
}
err = rw.WriteUVariant(writer, uint64(index[code]))
if err != nil {
return err
}
err = rw.WriteUVariant(writer, uint64(len(domains[code])))
if err != nil {
return err
}
}
_, err = writer.Write(content.Bytes()) _, err = writer.Write(content.Bytes())
if err != nil { if err != nil {
return err return err

113
common/sniff/bittorrent.go Normal file
View File

@ -0,0 +1,113 @@
package sniff
import (
"bytes"
"context"
"encoding/binary"
"io"
"os"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
)
const (
trackerConnectFlag = iota
trackerAnnounceFlag
trackerScrapeFlag
trackerProtocolID = 0x41727101980
trackerConnectMinSize = 16
trackerAnnounceMinSize = 20
trackerScrapeMinSize = 8
)
// BitTorrent detects if the stream is a BitTorrent connection.
// For the BitTorrent protocol specification, see https://www.bittorrent.org/beps/bep_0003.html
func BitTorrent(_ context.Context, reader io.Reader) (*adapter.InboundContext, error) {
var first byte
err := binary.Read(reader, binary.BigEndian, &first)
if err != nil {
return nil, err
}
if first != 19 {
return nil, os.ErrInvalid
}
var protocol [19]byte
_, err = reader.Read(protocol[:])
if err != nil {
return nil, err
}
if string(protocol[:]) != "BitTorrent protocol" {
return nil, os.ErrInvalid
}
return &adapter.InboundContext{
Protocol: C.ProtocolBitTorrent,
}, nil
}
// UTP detects if the packet is a uTP connection packet.
// For the uTP protocol specification, see
// 1. https://www.bittorrent.org/beps/bep_0029.html
// 2. https://github.com/bittorrent/libutp/blob/2b364cbb0650bdab64a5de2abb4518f9f228ec44/utp_internal.cpp#L112
func UTP(_ context.Context, packet []byte) (*adapter.InboundContext, error) {
// A valid uTP packet must be at least 20 bytes long.
if len(packet) < 20 {
return nil, os.ErrInvalid
}
version := packet[0] & 0x0F
ty := packet[0] >> 4
if version != 1 || ty > 4 {
return nil, os.ErrInvalid
}
// Validate the extensions
extension := packet[1]
reader := bytes.NewReader(packet[20:])
for extension != 0 {
err := binary.Read(reader, binary.BigEndian, &extension)
if err != nil {
return nil, err
}
var length byte
err = binary.Read(reader, binary.BigEndian, &length)
if err != nil {
return nil, err
}
_, err = reader.Seek(int64(length), io.SeekCurrent)
if err != nil {
return nil, err
}
}
return &adapter.InboundContext{
Protocol: C.ProtocolBitTorrent,
}, nil
}
// UDPTracker detects if the packet is a UDP Tracker Protocol packet.
// For the UDP Tracker Protocol specification, see https://www.bittorrent.org/beps/bep_0015.html
func UDPTracker(_ context.Context, packet []byte) (*adapter.InboundContext, error) {
switch {
case len(packet) >= trackerConnectMinSize &&
binary.BigEndian.Uint64(packet[:8]) == trackerProtocolID &&
binary.BigEndian.Uint32(packet[8:12]) == trackerConnectFlag:
fallthrough
case len(packet) >= trackerAnnounceMinSize &&
binary.BigEndian.Uint32(packet[8:12]) == trackerAnnounceFlag:
fallthrough
case len(packet) >= trackerScrapeMinSize &&
binary.BigEndian.Uint32(packet[8:12]) == trackerScrapeFlag:
return &adapter.InboundContext{
Protocol: C.ProtocolBitTorrent,
}, nil
default:
return nil, os.ErrInvalid
}
}

View File

@ -0,0 +1,81 @@
package sniff_test
import (
"bytes"
"context"
"encoding/hex"
"testing"
"github.com/sagernet/sing-box/common/sniff"
C "github.com/sagernet/sing-box/constant"
"github.com/stretchr/testify/require"
)
func TestSniffBittorrent(t *testing.T) {
t.Parallel()
packets := []string{
"13426974546f7272656e742070726f746f636f6c0000000000100000e21ea9569b69bab33c97851d0298bdfa89bc90922d5554313631302dea812fcd6a3563e3be40c1d1",
"13426974546f7272656e742070726f746f636f6c00000000001000052aa4f5a7e209e54b32803d43670971c4c8caaa052d5452333030302d653369733079647675763638",
"13426974546f7272656e742070726f746f636f6c00000000001000052aa4f5a7e209e54b32803d43670971c4c8caaa052d5452343035302d6f7a316c6e79377931716130",
}
for _, pkt := range packets {
pkt, err := hex.DecodeString(pkt)
require.NoError(t, err)
metadata, err := sniff.BitTorrent(context.TODO(), bytes.NewReader(pkt))
require.NoError(t, err)
require.Equal(t, C.ProtocolBitTorrent, metadata.Protocol)
}
}
func TestSniffUTP(t *testing.T) {
t.Parallel()
packets := []string{
"010041a282d7ee7b583afb160004000006d8318da776968f92d666f7963f32dae23ba0d2c810d8b8209cc4939f54fde9eeaa521c2c20c9ba7f43f4fb0375f28de06643b5e3ca4685ab7ac76adca99783be72ef05ed59ef4234f5712b75b4c7c0d7bee8fe2ca20ad626ba5bb0ffcc16bf06790896f888048cf72716419a07db1a3dca4550fbcea75b53e97235168a221cf3e553dfbb723961bd719fab038d86e0ecb74747f5a2cd669de1c4b9ad375f3a492d09d98cdfad745435625401315bbba98d35d32086299801377b93495a63a9efddb8d05f5b37a5c5b1c0a25e917f12007bb5e05013ada8aff544fab8cadf61d80ddb0b60f12741e44515a109d144fd53ef845acb4b5ccf0d6fc302d7003d76df3fc3423bb0237301c9e88f900c2d392a8e0fdb36d143cf7527a93fd0a2638b746e72f6699fffcd4fd15348fce780d4caa04382fd9faf1ca0ae377ca805da7536662b84f5ee18dd3ae38fcb095a7543e55f9069ae92c8cf54ae44e97b558d35e2545c66601ed2149cbc32bd6df199a2be7cf0da8b2ff137e0d23e776bc87248425013876d3a3cc31a83b424b752bd0346437f24b532978005d8f5b1b0be1a37a2489c32a18a9ad3118e3f9d30eb299bffae18e1f0677c2a5c185e62519093fe6bc2b7339299ea50a587989f726ca6443a75dd5bb936f6367c6355d80fae53ff529d740b2e5576e3eefdf1fdbfc69c3c8d8ac750512635de63e054bee1d3b689bc1b2bc3d2601e42a00b5c89066d173d4ae7ffedfd2274e5cf6d868fbe640aedb69b8246142f00b32d459974287537ddd5373460dcbc92f5cfdd7a3ed6020822ae922d947893752ca1983d0d32977374c384ac8f5ab566859019b7351526b9f13e932037a55bb052d9deb3b3c23317e0784fdc51a64f2159bfea3b069cf5caf02ee2c3c1a6b6b427bb16165713e8802d95b5c8ed77953690e994bd38c9ae113fedaf6ee7fc2b96c032ceafc2a530ad0422e84546b9c6ad8ef6ea02fa508abddd1805c38a7b42e9b7c971b1b636865ebec06ed754bb404cd6b4e6cc8cb77bd4a0c43410d5cd5ef8fe853a66d49b3b9e06cb141236cdbfdd5761601dc54d1250b86c660e0f898fe62526fdd9acf0eab60a3bbbb2151970461f28f10b31689594bea646c4b03ee197d63bdef4e5a7c22716b3bb9494a83b78ecd81b338b80ac6c09c43485b1b09ba41c74343832c78f0520c1d659ac9eb1502094141e82fb9e5e620970ebc0655514c43c294a7714cbf9a499d277daf089f556398a01589a77494bec8bfb60a108f3813b55368672b88c1af40f6b3c8b513f7c70c3e0efce85228b8b9ec67ba0393f9f7305024d8e2da6a26cf85613d14f249170ce1000089df4c9c260df7f8292aa2ecb5d5bac97656d59aa248caedea2d198e51ce87baece338716d114b458de02d65c9ff808ca5b5b73723b4d1e962d9ac2d98176544dc9984cf8554d07820ef3dd0861cfe57b478328046380de589adad94ee44743ffac73bb7361feca5d56f07cf8ce75080e261282ae30350d7882679b15cab9e7e53ddf93310b33f7390ae5d318bb53f387e6af5d0ef4f947fc9cb8e7e38b52c7f8d772ece6156b38d88796ea19df02c53723b44df7c76315a0de9462f27287e682d2b4cda1a68fe00d7e48c51ee981be44e1ca940fb5190c12655edb4a83c3a4f33e48a015692df4f0b3d61656e362aca657b5ae8c12db5a0db3db1e45135ee918b66918f40e53c4f83e9da0cddfe63f736ae751ab3837a30ae3220d8e8e311487093a7b90c7e7e40dd54ca750e19452f9193aa892aa6a6229ab493dadae988b1724f7898ee69c36d3eb7364c4adbeca811cfe2065873e78c2b6dfdf1595f7a7831c07e03cda82e4f86f76438dfb2b07c13638ce7b509cfa71b88b5102b39a203b423202088e1c2103319cb32c13c1e546ff8612fa194c95a7808ab767c265a1bd5fa0efed5c8ec1701876a00ec8",
"01001ecb68176f215d04326300100000dbcf30292d14b54e9ee2d115ee5b8ebc7fad3e882d4fcdd0c14c6b917c11cb4c6a9f410b52a33ae97c2ac77c7a2b122b8955e09af3c5c595f1b2e79ca57cfe44c44e069610773b9bc9ba223d7f6b383e3adddd03fb88a8476028e30979c2ef321ffc97c5c132bcf9ac5b410bbb5ec6cefca3c7209202a14c5ae922b6b157b0a80249d13ffe5b996af0bc8e54ba576d148372494303e7ead0602b05b9c8fc97d48508a028a04d63a1fd28b0edfcd5c51715f63188b53eefede98a76912dca98518551a8856567307a56a702cbfcc115ea0c755b418bc2c7b57721239b82f09fb24328a4b0ce0f109bcb2a64e04b8aadb1f8487585425acdf8fc4ec8ea93cfcec5ac098bb29d42ddef6e46b03f34a5de28316726699b7cb5195c33e5c48abe87d591d63f9991c84c30819d186d6e0e95fd83c8dff07aa669c4430989bcaccfeacb9bcadbdb4d8f1964dbeb9687745656edd30b21c66cc0a1d742a78717d134a19a7f02d285a4973b1a198c00cfdff4676608dc4f3e817e3463c3b4e2c80d3e8d4fbac541a58a2fb7ad6939f607f8144eff6c8b0adc28ee5609ea158987519892fb",
"21001ecb6817f2805d044fd700100000dbd03029",
"410277ef0b1fb1f60000000000040000c233000000080000000000000000",
}
for _, pkt := range packets {
pkt, err := hex.DecodeString(pkt)
require.NoError(t, err)
metadata, err := sniff.UTP(context.TODO(), pkt)
require.NoError(t, err)
require.Equal(t, C.ProtocolBitTorrent, metadata.Protocol)
}
}
func TestSniffUDPTracker(t *testing.T) {
t.Parallel()
connectPackets := []string{
// connect packets
"00000417271019800000000078e90560",
"00000417271019800000000022c5d64d",
"000004172710198000000000b3863541",
// announce packets
"3d7592ead4b8c9e300000001b871a3820000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002092f616e6e6f756e6365",
"3d7592ead4b8c9e30000000188deed1c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002092f616e6e6f756e6365",
"3d7592ead4b8c9e300000001ceb948ad0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a3362cdb7020ff920e5aa642c3d4066950dd1f01f4d00000000000000000000000000000000000000000000000000000000000000000000000000000000000002092f616e6e6f756e6365",
// scrape packets
"3d7592ead4b8c9e300000002d2f4bba5a94a8fe5ccb19ba61c4c0873d391e987982fbbd3",
"3d7592ead4b8c9e300000002441243292aae6c35c94fcfb415dbe95f408b9ce91ee846ed",
"3d7592ead4b8c9e300000002b2aa461b1ad1fa9661cf3fe45fb2504ad52ec6c67758e294",
}
for _, pkt := range connectPackets {
pkt, err := hex.DecodeString(pkt)
require.NoError(t, err)
metadata, err := sniff.UDPTracker(context.TODO(), pkt)
require.NoError(t, err)
require.Equal(t, C.ProtocolBitTorrent, metadata.Protocol)
}
}

31
common/sniff/dtls.go Normal file
View File

@ -0,0 +1,31 @@
package sniff
import (
"context"
"os"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
)
func DTLSRecord(ctx context.Context, packet []byte) (*adapter.InboundContext, error) {
const fixedHeaderSize = 13
if len(packet) < fixedHeaderSize {
return nil, os.ErrInvalid
}
contentType := packet[0]
switch contentType {
case 20, 21, 22, 23, 25:
default:
return nil, os.ErrInvalid
}
versionMajor := packet[1]
if versionMajor != 0xfe {
return nil, os.ErrInvalid
}
versionMinor := packet[2]
if versionMinor != 0xff && versionMinor != 0xfd {
return nil, os.ErrInvalid
}
return &adapter.InboundContext{Protocol: C.ProtocolDTLS}, nil
}

30
common/sniff/dtls_test.go Normal file
View File

@ -0,0 +1,30 @@
package sniff_test
import (
"context"
"encoding/hex"
"testing"
"github.com/sagernet/sing-box/common/sniff"
C "github.com/sagernet/sing-box/constant"
"github.com/stretchr/testify/require"
)
func TestSniffDTLSClientHello(t *testing.T) {
t.Parallel()
packet, err := hex.DecodeString("16fefd0000000000000000007e010000720000000000000072fefd668a43523798e064bd806d0c87660de9c611a59bbdfc3892c4e072d94f2cafc40000000cc02bc02fc00ac014c02cc0300100003c000d0010000e0403050306030401050106010807ff01000100000a00080006001d00170018000b00020100000e000900060008000700010000170000")
require.NoError(t, err)
metadata, err := sniff.DTLSRecord(context.Background(), packet)
require.NoError(t, err)
require.Equal(t, metadata.Protocol, C.ProtocolDTLS)
}
func TestSniffDTLSClientApplicationData(t *testing.T) {
t.Parallel()
packet, err := hex.DecodeString("17fefd000100000000000100440001000000000001a4f682b77ecadd10f3f3a2f78d90566212366ff8209fd77314f5a49352f9bb9bd12f4daba0b4736ae29e46b9714d3b424b3e6d0234736619b5aa0d3f")
require.NoError(t, err)
metadata, err := sniff.DTLSRecord(context.Background(), packet)
require.NoError(t, err)
require.Equal(t, metadata.Protocol, C.ProtocolDTLS)
}

View File

@ -1,6 +1,7 @@
package srs package srs
import ( import (
"bufio"
"compress/zlib" "compress/zlib"
"encoding/binary" "encoding/binary"
"io" "io"
@ -11,7 +12,7 @@ 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/rw" "github.com/sagernet/sing/common/varbin"
"go4.org/netipx" "go4.org/netipx"
) )
@ -38,14 +39,14 @@ const (
ruleItemFinal uint8 = 0xFF ruleItemFinal uint8 = 0xFF
) )
func Read(reader io.Reader, recovery bool) (ruleSet option.PlainRuleSet, err error) { func Read(reader io.Reader, recover bool) (ruleSet option.PlainRuleSet, err error) {
var magicBytes [3]byte var magicBytes [3]byte
_, err = io.ReadFull(reader, magicBytes[:]) _, err = io.ReadFull(reader, magicBytes[:])
if err != nil { if err != nil {
return return
} }
if magicBytes != MagicBytes { if magicBytes != MagicBytes {
err = E.New("invalid sing-box rule set file") err = E.New("invalid sing-box rule-set file")
return return
} }
var version uint8 var version uint8
@ -60,13 +61,14 @@ func Read(reader io.Reader, recovery bool) (ruleSet option.PlainRuleSet, err err
if err != nil { if err != nil {
return return
} }
length, err := rw.ReadUVariant(zReader) bReader := bufio.NewReader(zReader)
length, err := binary.ReadUvarint(bReader)
if err != nil { if err != nil {
return return
} }
ruleSet.Rules = make([]option.HeadlessRule, length) ruleSet.Rules = make([]option.HeadlessRule, length)
for i := uint64(0); i < length; i++ { for i := uint64(0); i < length; i++ {
ruleSet.Rules[i], err = readRule(zReader, recovery) ruleSet.Rules[i], err = readRule(bReader, recover)
if err != nil { if err != nil {
err = E.Cause(err, "read rule[", i, "]") err = E.Cause(err, "read rule[", i, "]")
return return
@ -88,20 +90,25 @@ func Write(writer io.Writer, ruleSet option.PlainRuleSet) error {
if err != nil { if err != nil {
return err return err
} }
err = rw.WriteUVariant(zWriter, uint64(len(ruleSet.Rules))) bWriter := bufio.NewWriter(zWriter)
_, err = varbin.WriteUvarint(bWriter, uint64(len(ruleSet.Rules)))
if err != nil { if err != nil {
return err return err
} }
for _, rule := range ruleSet.Rules { for _, rule := range ruleSet.Rules {
err = writeRule(zWriter, rule) err = writeRule(bWriter, rule)
if err != nil { if err != nil {
return err return err
} }
} }
err = bWriter.Flush()
if err != nil {
return err
}
return zWriter.Close() return zWriter.Close()
} }
func readRule(reader io.Reader, recovery bool) (rule option.HeadlessRule, err error) { func readRule(reader varbin.Reader, recover bool) (rule option.HeadlessRule, err error) {
var ruleType uint8 var ruleType uint8
err = binary.Read(reader, binary.BigEndian, &ruleType) err = binary.Read(reader, binary.BigEndian, &ruleType)
if err != nil { if err != nil {
@ -110,17 +117,17 @@ func readRule(reader io.Reader, recovery bool) (rule option.HeadlessRule, err er
switch ruleType { switch ruleType {
case 0: case 0:
rule.Type = C.RuleTypeDefault rule.Type = C.RuleTypeDefault
rule.DefaultOptions, err = readDefaultRule(reader, recovery) rule.DefaultOptions, err = readDefaultRule(reader, recover)
case 1: case 1:
rule.Type = C.RuleTypeLogical rule.Type = C.RuleTypeLogical
rule.LogicalOptions, err = readLogicalRule(reader, recovery) rule.LogicalOptions, err = readLogicalRule(reader, recover)
default: default:
err = E.New("unknown rule type: ", ruleType) err = E.New("unknown rule type: ", ruleType)
} }
return return
} }
func writeRule(writer io.Writer, rule option.HeadlessRule) error { func writeRule(writer varbin.Writer, rule option.HeadlessRule) error {
switch rule.Type { switch rule.Type {
case C.RuleTypeDefault: case C.RuleTypeDefault:
return writeDefaultRule(writer, rule.DefaultOptions) return writeDefaultRule(writer, rule.DefaultOptions)
@ -131,7 +138,7 @@ func writeRule(writer io.Writer, rule option.HeadlessRule) error {
} }
} }
func readDefaultRule(reader io.Reader, recovery bool) (rule option.DefaultHeadlessRule, err error) { func readDefaultRule(reader varbin.Reader, recover bool) (rule option.DefaultHeadlessRule, err error) {
var lastItemType uint8 var lastItemType uint8
for { for {
var itemType uint8 var itemType uint8
@ -158,6 +165,9 @@ func readDefaultRule(reader io.Reader, recovery bool) (rule option.DefaultHeadle
return return
} }
rule.DomainMatcher = matcher rule.DomainMatcher = matcher
if recover {
rule.Domain, rule.DomainSuffix = matcher.Dump()
}
case ruleItemDomainKeyword: case ruleItemDomainKeyword:
rule.DomainKeyword, err = readRuleItemString(reader) rule.DomainKeyword, err = readRuleItemString(reader)
case ruleItemDomainRegex: case ruleItemDomainRegex:
@ -167,7 +177,7 @@ func readDefaultRule(reader io.Reader, recovery bool) (rule option.DefaultHeadle
if err != nil { if err != nil {
return return
} }
if recovery { if recover {
rule.SourceIPCIDR = common.Map(rule.SourceIPSet.Prefixes(), netip.Prefix.String) rule.SourceIPCIDR = common.Map(rule.SourceIPSet.Prefixes(), netip.Prefix.String)
} }
case ruleItemIPCIDR: case ruleItemIPCIDR:
@ -175,7 +185,7 @@ func readDefaultRule(reader io.Reader, recovery bool) (rule option.DefaultHeadle
if err != nil { if err != nil {
return return
} }
if recovery { if recover {
rule.IPCIDR = common.Map(rule.IPSet.Prefixes(), netip.Prefix.String) rule.IPCIDR = common.Map(rule.IPSet.Prefixes(), netip.Prefix.String)
} }
case ruleItemSourcePort: case ruleItemSourcePort:
@ -209,7 +219,7 @@ func readDefaultRule(reader io.Reader, recovery bool) (rule option.DefaultHeadle
} }
} }
func writeDefaultRule(writer io.Writer, rule option.DefaultHeadlessRule) error { func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule) error {
err := binary.Write(writer, binary.BigEndian, uint8(0)) err := binary.Write(writer, binary.BigEndian, uint8(0))
if err != nil { if err != nil {
return err return err
@ -327,73 +337,31 @@ func writeDefaultRule(writer io.Writer, rule option.DefaultHeadlessRule) error {
return nil return nil
} }
func readRuleItemString(reader io.Reader) ([]string, error) { func readRuleItemString(reader varbin.Reader) ([]string, error) {
length, err := rw.ReadUVariant(reader) return varbin.ReadValue[[]string](reader, binary.BigEndian)
if err != nil {
return nil, err
}
value := make([]string, length)
for i := uint64(0); i < length; i++ {
value[i], err = rw.ReadVString(reader)
if err != nil {
return nil, err
}
}
return value, nil
} }
func writeRuleItemString(writer io.Writer, itemType uint8, value []string) error { func writeRuleItemString(writer varbin.Writer, itemType uint8, value []string) error {
err := binary.Write(writer, binary.BigEndian, itemType) err := writer.WriteByte(itemType)
if err != nil { if err != nil {
return err return err
} }
err = rw.WriteUVariant(writer, uint64(len(value))) return varbin.Write(writer, binary.BigEndian, value)
}
func readRuleItemUint16(reader varbin.Reader) ([]uint16, error) {
return varbin.ReadValue[[]uint16](reader, binary.BigEndian)
}
func writeRuleItemUint16(writer varbin.Writer, itemType uint8, value []uint16) error {
err := writer.WriteByte(itemType)
if err != nil { if err != nil {
return err return err
} }
for _, item := range value { return varbin.Write(writer, binary.BigEndian, value)
err = rw.WriteVString(writer, item)
if err != nil {
return err
}
}
return nil
} }
func readRuleItemUint16(reader io.Reader) ([]uint16, error) { func writeRuleItemCIDR(writer varbin.Writer, itemType uint8, value []string) error {
length, err := rw.ReadUVariant(reader)
if err != nil {
return nil, err
}
value := make([]uint16, length)
for i := uint64(0); i < length; i++ {
err = binary.Read(reader, binary.BigEndian, &value[i])
if err != nil {
return nil, err
}
}
return value, nil
}
func writeRuleItemUint16(writer io.Writer, itemType uint8, value []uint16) error {
err := binary.Write(writer, binary.BigEndian, itemType)
if err != nil {
return err
}
err = rw.WriteUVariant(writer, uint64(len(value)))
if err != nil {
return err
}
for _, item := range value {
err = binary.Write(writer, binary.BigEndian, item)
if err != nil {
return err
}
}
return nil
}
func writeRuleItemCIDR(writer io.Writer, itemType uint8, value []string) error {
var builder netipx.IPSetBuilder var builder netipx.IPSetBuilder
for i, prefixString := range value { for i, prefixString := range value {
prefix, err := netip.ParsePrefix(prefixString) prefix, err := netip.ParsePrefix(prefixString)
@ -419,9 +387,8 @@ func writeRuleItemCIDR(writer io.Writer, itemType uint8, value []string) error {
return writeIPSet(writer, ipSet) return writeIPSet(writer, ipSet)
} }
func readLogicalRule(reader io.Reader, recovery bool) (logicalRule option.LogicalHeadlessRule, err error) { func readLogicalRule(reader varbin.Reader, recovery bool) (logicalRule option.LogicalHeadlessRule, err error) {
var mode uint8 mode, err := reader.ReadByte()
err = binary.Read(reader, binary.BigEndian, &mode)
if err != nil { if err != nil {
return return
} }
@ -434,7 +401,7 @@ func readLogicalRule(reader io.Reader, recovery bool) (logicalRule option.Logica
err = E.New("unknown logical mode: ", mode) err = E.New("unknown logical mode: ", mode)
return return
} }
length, err := rw.ReadUVariant(reader) length, err := binary.ReadUvarint(reader)
if err != nil { if err != nil {
return return
} }
@ -453,7 +420,7 @@ func readLogicalRule(reader io.Reader, recovery bool) (logicalRule option.Logica
return return
} }
func writeLogicalRule(writer io.Writer, logicalRule option.LogicalHeadlessRule) error { func writeLogicalRule(writer varbin.Writer, logicalRule option.LogicalHeadlessRule) error {
err := binary.Write(writer, binary.BigEndian, uint8(1)) err := binary.Write(writer, binary.BigEndian, uint8(1))
if err != nil { if err != nil {
return err return err
@ -469,7 +436,7 @@ func writeLogicalRule(writer io.Writer, logicalRule option.LogicalHeadlessRule)
if err != nil { if err != nil {
return err return err
} }
err = rw.WriteUVariant(writer, uint64(len(logicalRule.Rules))) _, err = varbin.WriteUvarint(writer, uint64(len(logicalRule.Rules)))
if err != nil { if err != nil {
return err return err
} }

View File

@ -2,11 +2,13 @@ package srs
import ( import (
"encoding/binary" "encoding/binary"
"io"
"net/netip" "net/netip"
"os"
"unsafe" "unsafe"
"github.com/sagernet/sing/common/rw" "github.com/sagernet/sing/common"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/common/varbin"
"go4.org/netipx" "go4.org/netipx"
) )
@ -20,94 +22,57 @@ type myIPRange struct {
to netip.Addr to netip.Addr
} }
func readIPSet(reader io.Reader) (*netipx.IPSet, error) { type myIPRangeData struct {
var version uint8 From []byte
err := binary.Read(reader, binary.BigEndian, &version) To []byte
}
func readIPSet(reader varbin.Reader) (*netipx.IPSet, error) {
version, err := reader.ReadByte()
if err != nil { if err != nil {
return nil, err return nil, err
} }
if version != 1 {
return nil, os.ErrInvalid
}
// WTF why using uint64 here
var length uint64 var length uint64
err = binary.Read(reader, binary.BigEndian, &length) err = binary.Read(reader, binary.BigEndian, &length)
if err != nil { if err != nil {
return nil, err return nil, err
} }
mySet := &myIPSet{ ranges := make([]myIPRangeData, length)
rr: make([]myIPRange, length), err = varbin.Read(reader, binary.BigEndian, &ranges)
if err != nil {
return nil, err
} }
for i := uint64(0); i < length; i++ { mySet := &myIPSet{
var ( rr: make([]myIPRange, len(ranges)),
fromLen uint64 }
toLen uint64 for i, rangeData := range ranges {
fromAddr netip.Addr mySet.rr[i].from = M.AddrFromIP(rangeData.From)
toAddr netip.Addr mySet.rr[i].to = M.AddrFromIP(rangeData.To)
)
fromLen, err = rw.ReadUVariant(reader)
if err != nil {
return nil, err
}
fromBytes := make([]byte, fromLen)
_, err = io.ReadFull(reader, fromBytes)
if err != nil {
return nil, err
}
err = fromAddr.UnmarshalBinary(fromBytes)
if err != nil {
return nil, err
}
toLen, err = rw.ReadUVariant(reader)
if err != nil {
return nil, err
}
toBytes := make([]byte, toLen)
_, err = io.ReadFull(reader, toBytes)
if err != nil {
return nil, err
}
err = toAddr.UnmarshalBinary(toBytes)
if err != nil {
return nil, err
}
mySet.rr[i] = myIPRange{fromAddr, toAddr}
} }
return (*netipx.IPSet)(unsafe.Pointer(mySet)), nil return (*netipx.IPSet)(unsafe.Pointer(mySet)), nil
} }
func writeIPSet(writer io.Writer, set *netipx.IPSet) error { func writeIPSet(writer varbin.Writer, set *netipx.IPSet) error {
err := binary.Write(writer, binary.BigEndian, uint8(1)) err := writer.WriteByte(1)
if err != nil { if err != nil {
return err return err
} }
mySet := (*myIPSet)(unsafe.Pointer(set)) dataList := common.Map((*myIPSet)(unsafe.Pointer(set)).rr, func(rr myIPRange) myIPRangeData {
err = binary.Write(writer, binary.BigEndian, uint64(len(mySet.rr))) return myIPRangeData{
From: rr.from.AsSlice(),
To: rr.to.AsSlice(),
}
})
err = binary.Write(writer, binary.BigEndian, uint64(len(dataList)))
if err != nil { if err != nil {
return err return err
} }
for _, rr := range mySet.rr { for _, data := range dataList {
var ( err = varbin.Write(writer, binary.BigEndian, data)
fromBinary []byte
toBinary []byte
)
fromBinary, err = rr.from.MarshalBinary()
if err != nil {
return err
}
err = rw.WriteUVariant(writer, uint64(len(fromBinary)))
if err != nil {
return err
}
_, err = writer.Write(fromBinary)
if err != nil {
return err
}
toBinary, err = rr.to.MarshalBinary()
if err != nil {
return err
}
err = rw.WriteUVariant(writer, uint64(len(toBinary)))
if err != nil {
return err
}
_, err = writer.Write(toBinary)
if err != nil { if err != nil {
return err return err
} }

View File

@ -11,12 +11,11 @@ import (
"strings" "strings"
cftls "github.com/sagernet/cloudflare-tls" cftls "github.com/sagernet/cloudflare-tls"
"github.com/sagernet/fswatch"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/ntp" "github.com/sagernet/sing/common/ntp"
"github.com/fsnotify/fsnotify"
) )
type echServerConfig struct { type echServerConfig struct {
@ -26,9 +25,8 @@ type echServerConfig struct {
key []byte key []byte
certificatePath string certificatePath string
keyPath string keyPath string
watcher *fsnotify.Watcher
echKeyPath string echKeyPath string
echWatcher *fsnotify.Watcher watcher *fswatch.Watcher
} }
func (c *echServerConfig) ServerName() string { func (c *echServerConfig) ServerName() string {
@ -66,146 +64,84 @@ func (c *echServerConfig) Clone() Config {
} }
func (c *echServerConfig) Start() error { func (c *echServerConfig) Start() error {
if c.certificatePath != "" && c.keyPath != "" { err := c.startWatcher()
err := c.startWatcher() if err != nil {
if err != nil { c.logger.Warn("create credentials watcher: ", err)
c.logger.Warn("create fsnotify watcher: ", err)
}
}
if c.echKeyPath != "" {
err := c.startECHWatcher()
if err != nil {
c.logger.Warn("create fsnotify watcher: ", err)
}
} }
return nil return nil
} }
func (c *echServerConfig) startWatcher() error { func (c *echServerConfig) startWatcher() error {
watcher, err := fsnotify.NewWatcher() var watchPath []string
if err != nil {
return err
}
if c.certificatePath != "" { if c.certificatePath != "" {
err = watcher.Add(c.certificatePath) watchPath = append(watchPath, c.certificatePath)
if err != nil {
return err
}
} }
if c.keyPath != "" { if c.keyPath != "" {
err = watcher.Add(c.keyPath) watchPath = append(watchPath, c.keyPath)
if err != nil { }
return err 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) {
err := c.credentialsUpdated(path)
if err != nil {
c.logger.Error(E.Cause(err, "reload credentials from ", path))
}
},
})
if err != nil {
return err
} }
c.watcher = watcher c.watcher = watcher
go c.loopUpdate()
return nil return nil
} }
func (c *echServerConfig) loopUpdate() { func (c *echServerConfig) credentialsUpdated(path string) error {
for { if path == c.certificatePath || path == c.keyPath {
select { if path == c.certificatePath {
case event, ok := <-c.watcher.Events: certificate, err := os.ReadFile(c.certificatePath)
if !ok {
return
}
if event.Op&fsnotify.Write != fsnotify.Write {
continue
}
err := c.reloadKeyPair()
if err != nil { if err != nil {
c.logger.Error(E.Cause(err, "reload TLS key pair")) return err
} }
case err, ok := <-c.watcher.Errors: c.certificate = certificate
if !ok { } else {
return key, err := os.ReadFile(c.keyPath)
}
c.logger.Error(E.Cause(err, "fsnotify error"))
}
}
}
func (c *echServerConfig) reloadKeyPair() error {
if c.certificatePath != "" {
certificate, err := os.ReadFile(c.certificatePath)
if err != nil {
return E.Cause(err, "reload certificate from ", c.certificatePath)
}
c.certificate = certificate
}
if c.keyPath != "" {
key, err := os.ReadFile(c.keyPath)
if err != nil {
return E.Cause(err, "reload key from ", c.keyPath)
}
c.key = key
}
keyPair, err := cftls.X509KeyPair(c.certificate, c.key)
if err != nil {
return E.Cause(err, "reload key pair")
}
c.config.Certificates = []cftls.Certificate{keyPair}
c.logger.Info("reloaded TLS certificate")
return nil
}
func (c *echServerConfig) startECHWatcher() error {
watcher, err := fsnotify.NewWatcher()
if err != nil {
return err
}
err = watcher.Add(c.echKeyPath)
if err != nil {
return err
}
c.echWatcher = watcher
go c.loopECHUpdate()
return nil
}
func (c *echServerConfig) loopECHUpdate() {
for {
select {
case event, ok := <-c.echWatcher.Events:
if !ok {
return
}
if event.Op&fsnotify.Write != fsnotify.Write {
continue
}
err := c.reloadECHKey()
if err != nil { if err != nil {
c.logger.Error(E.Cause(err, "reload ECH key")) return err
} }
case err, ok := <-c.echWatcher.Errors: c.key = key
if !ok {
return
}
c.logger.Error(E.Cause(err, "fsnotify error"))
} }
keyPair, err := cftls.X509KeyPair(c.certificate, c.key)
if err != nil {
return E.Cause(err, "parse key pair")
}
c.config.Certificates = []cftls.Certificate{keyPair}
c.logger.Info("reloaded TLS certificate")
} else {
echKeyContent, err := os.ReadFile(c.echKeyPath)
if err != nil {
return err
}
block, rest := pem.Decode(echKeyContent)
if block == nil || block.Type != "ECH KEYS" || len(rest) > 0 {
return E.New("invalid ECH keys pem")
}
echKeys, err := cftls.EXP_UnmarshalECHKeys(block.Bytes)
if err != nil {
return E.Cause(err, "parse ECH keys")
}
echKeySet, err := cftls.EXP_NewECHKeySet(echKeys)
if err != nil {
return E.Cause(err, "create ECH key set")
}
c.config.ServerECHProvider = echKeySet
c.logger.Info("reloaded ECH keys")
} }
}
func (c *echServerConfig) reloadECHKey() error {
echKeyContent, err := os.ReadFile(c.echKeyPath)
if err != nil {
return err
}
block, rest := pem.Decode(echKeyContent)
if block == nil || block.Type != "ECH KEYS" || len(rest) > 0 {
return E.New("invalid ECH keys pem")
}
echKeys, err := cftls.EXP_UnmarshalECHKeys(block.Bytes)
if err != nil {
return E.Cause(err, "parse ECH keys")
}
echKeySet, err := cftls.EXP_NewECHKeySet(echKeys)
if err != nil {
return E.Cause(err, "create ECH key set")
}
c.config.ServerECHProvider = echKeySet
c.logger.Info("reloaded ECH keys")
return nil return nil
} }
@ -213,12 +149,7 @@ func (c *echServerConfig) Close() error {
var err error var err error
if c.watcher != nil { if c.watcher != nil {
err = E.Append(err, c.watcher.Close(), func(err error) error { err = E.Append(err, c.watcher.Close(), func(err error) error {
return E.Cause(err, "close certificate watcher") return E.Cause(err, "close credentials watcher")
})
}
if c.echWatcher != nil {
err = E.Append(err, c.echWatcher.Close(), func(err error) error {
return E.Cause(err, "close ECH key watcher")
}) })
} }
return err return err

View File

@ -7,14 +7,13 @@ import (
"os" "os"
"strings" "strings"
"github.com/sagernet/fswatch"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"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/common" "github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/ntp" "github.com/sagernet/sing/common/ntp"
"github.com/fsnotify/fsnotify"
) )
var errInsecureUnused = E.New("tls: insecure unused") var errInsecureUnused = E.New("tls: insecure unused")
@ -27,7 +26,7 @@ type STDServerConfig struct {
key []byte key []byte
certificatePath string certificatePath string
keyPath string keyPath string
watcher *fsnotify.Watcher watcher *fswatch.Watcher
} }
func (c *STDServerConfig) ServerName() string { func (c *STDServerConfig) ServerName() string {
@ -88,59 +87,37 @@ func (c *STDServerConfig) Start() error {
} }
func (c *STDServerConfig) startWatcher() error { func (c *STDServerConfig) startWatcher() error {
watcher, err := fsnotify.NewWatcher() var watchPath []string
if c.certificatePath != "" {
watchPath = append(watchPath, c.certificatePath)
}
if c.keyPath != "" {
watchPath = append(watchPath, c.keyPath)
}
watcher, err := fswatch.NewWatcher(fswatch.Options{
Path: watchPath,
Callback: func(path string) {
err := c.certificateUpdated(path)
if err != nil {
c.logger.Error(err)
}
},
})
if err != nil { if err != nil {
return err return err
} }
if c.certificatePath != "" {
err = watcher.Add(c.certificatePath)
if err != nil {
return err
}
}
if c.keyPath != "" {
err = watcher.Add(c.keyPath)
if err != nil {
return err
}
}
c.watcher = watcher c.watcher = watcher
go c.loopUpdate()
return nil return nil
} }
func (c *STDServerConfig) loopUpdate() { func (c *STDServerConfig) certificateUpdated(path string) error {
for { if path == c.certificatePath {
select {
case event, ok := <-c.watcher.Events:
if !ok {
return
}
if event.Op&fsnotify.Write != fsnotify.Write {
continue
}
err := c.reloadKeyPair()
if err != nil {
c.logger.Error(E.Cause(err, "reload TLS key pair"))
}
case err, ok := <-c.watcher.Errors:
if !ok {
return
}
c.logger.Error(E.Cause(err, "fsnotify error"))
}
}
}
func (c *STDServerConfig) reloadKeyPair() error {
if c.certificatePath != "" {
certificate, err := os.ReadFile(c.certificatePath) certificate, err := os.ReadFile(c.certificatePath)
if err != nil { if err != nil {
return E.Cause(err, "reload certificate from ", c.certificatePath) return E.Cause(err, "reload certificate from ", c.certificatePath)
} }
c.certificate = certificate c.certificate = certificate
} } else if path == c.keyPath {
if c.keyPath != "" {
key, err := os.ReadFile(c.keyPath) key, err := os.ReadFile(c.keyPath)
if err != nil { if err != nil {
return E.Cause(err, "reload key from ", c.keyPath) return E.Cause(err, "reload key from ", c.keyPath)

View File

@ -13,14 +13,14 @@ var resourcePaths []string
func FindPath(name string) (string, bool) { func FindPath(name string) (string, bool) {
name = os.ExpandEnv(name) name = os.ExpandEnv(name)
if rw.FileExists(name) { if rw.IsFile(name) {
return name, true return name, true
} }
for _, dir := range resourcePaths { for _, dir := range resourcePaths {
if path := filepath.Join(dir, dirName, name); rw.FileExists(path) { if path := filepath.Join(dir, dirName, name); rw.IsFile(path) {
return path, true return path, true
} }
if path := filepath.Join(dir, name); rw.FileExists(path) { if path := filepath.Join(dir, name); rw.IsFile(path) {
return path, true return path, true
} }
} }

View File

@ -1,9 +1,11 @@
package constant package constant
const ( const (
ProtocolTLS = "tls" ProtocolTLS = "tls"
ProtocolHTTP = "http" ProtocolHTTP = "http"
ProtocolQUIC = "quic" ProtocolQUIC = "quic"
ProtocolDNS = "dns" ProtocolDNS = "dns"
ProtocolSTUN = "stun" ProtocolSTUN = "stun"
ProtocolBitTorrent = "bittorrent"
ProtocolDTLS = "dtls"
) )

View File

@ -32,6 +32,12 @@ const (
func ProxyDisplayName(proxyType string) string { func ProxyDisplayName(proxyType string) string {
switch proxyType { switch proxyType {
case TypeTun:
return "TUN"
case TypeRedirect:
return "Redirect"
case TypeTProxy:
return "TProxy"
case TypeDirect: case TypeDirect:
return "Direct" return "Direct"
case TypeBlock: case TypeBlock:
@ -42,6 +48,8 @@ func ProxyDisplayName(proxyType string) string {
return "SOCKS" return "SOCKS"
case TypeHTTP: case TypeHTTP:
return "HTTP" return "HTTP"
case TypeMixed:
return "Mixed"
case TypeShadowsocks: case TypeShadowsocks:
return "Shadowsocks" return "Shadowsocks"
case TypeVMess: case TypeVMess:

View File

@ -11,6 +11,7 @@ const (
) )
const ( const (
RuleSetTypeInline = "inline"
RuleSetTypeLocal = "local" RuleSetTypeLocal = "local"
RuleSetTypeRemote = "remote" RuleSetTypeRemote = "remote"
RuleSetVersion1 = 1 RuleSetVersion1 = 1

View File

@ -2,19 +2,183 @@
icon: material/alert-decagram icon: material/alert-decagram
--- ---
#### 1.10.0-alpha.20
* Add DTLS sniffer
* Fixes and improvements
#### 1.10.0-alpha.19
* Add `rule-set decompile` command
* Add IP address support for `rule-set match` command
* Fixes and improvements
#### 1.10.0-alpha.18
* Add new `inline` rule-set type **1**
* Add auto reload support for local rule-set
* Update fsnotify usages **2**
* Fixes and improvements
**1**:
The new [rule-set] type inline (which also becomes the default type)
allows you to write headless rules directly without creating a rule-set file.
[rule-set]: /configuration/rule-set/
**2**:
sing-box now uses fsnotify correctly and will not cancel watching
if the target file is deleted or recreated via rename (e.g. `mv`).
This affects all path options that support reload, including
`tls.certificate_path`, `tls.key_path`, `tls.ech.key_path` and `rule_set.path`.
#### 1.10.0-alpha.17
* Some chaotic changes **1**
* `rule_set_ipcidr_match_source` rule items are renamed **2**
* Add `rule_set_ip_cidr_accept_empty` DNS address filter rule item **3**
* Update quic-go to v0.45.1
* Fixes and improvements
**1**:
Something may be broken, please actively report problems with this version.
**2**:
`rule_set_ipcidr_match_source` route and DNS rule items are renamed to
`rule_set_ip_cidr_match_source` and will be remove in sing-box 1.11.0.
**3**:
See [DNS Rule](/configuration/dns/rule/#rule_set_ip_cidr_accept_empty).
#### 1.10.0-alpha.16
* Add custom options for `auto-route` and `auto-redirect` **1**
* Fixes and improvements
**1**:
See [iproute2_table_index](/configuration/inbound/tun/#iproute2_table_index),
[iproute2_rule_index](/configuration/inbound/tun/#iproute2_rule_index),
[auto_redirect_input_mark](/configuration/inbound/tun/#auto_redirect_input_mark) and
[auto_redirect_output_mark](/configuration/inbound/tun/#auto_redirect_output_mark).
#### 1.10.0-alpha.13
* TUN address fields are merged **1**
* Add route address set support for auto-redirect **2**
**1**:
See [Migration](/migration/#tun-address-fields-are-merged).
**2**:
The new feature will allow you to configure the destination IP CIDR rules
in the specified rule-sets to the firewall automatically.
Specified or unspecified destinations will bypass the sing-box routes to get better performance
(for example, keep hardware offloading of direct traffics on the router).
See [route_address_set](/configuration/inbound/tun/#route_address_set)
and [route_exclude_address_set](/configuration/inbound/tun/#route_exclude_address_set).
#### 1.10.0-alpha.12
* Fix auto-redirect not configuring nftables forward chain correctly
* Fixes and improvements
### 1.9.3 ### 1.9.3
* Fixes and improvements * Fixes and improvements
#### 1.10.0-alpha.10
* Fixes and improvements
### 1.9.2 ### 1.9.2
* Fixes and improvements * Fixes and improvements
#### 1.10.0-alpha.8
* Drop support for go1.18 and go1.19 **1**
* Update quic-go to v0.45.0
* Update Hysteria2 BBR congestion control
* Fixes and improvements
**1**:
Due to maintenance difficulties, sing-box 1.10.0 requires at least Go 1.20 to compile.
### 1.9.1 ### 1.9.1
* Fixes and improvements * Fixes and improvements
#### 1.10.0-alpha.7
* Fixes and improvements
#### 1.10.0-alpha.5
* Improve auto-redirect **1**
**1**:
nftables support and DNS hijacking has been added.
Tun inbounds with `auto_route` and `auto_redirect` now works as expected on routers **without intervention**.
#### 1.10.0-alpha.4
* Fix auto-redirect **1**
* Improve auto-route on linux **2**
**1**:
Tun inbounds with `auto_route` and `auto_redirect` now works as expected on routers.
**2**:
Tun inbounds with `auto_route` and `strict_route` now works as expected on routers and servers,
but the usages of [exclude_interface](/configuration/inbound/tun/#exclude_interface) need to be updated.
#### 1.10.0-alpha.2
* Move auto-redirect to Tun **1**
* Fixes and improvements
**1**:
Linux support are added.
See [Tun](/configuration/inbound/tun/#auto_redirect).
#### 1.10.0-alpha.1
* Add tailing comma support in JSON configuration
* Add simple auto-redirect for Android **1**
* Add BitTorrent sniffer **2**
**1**:
It allows you to use redirect inbound in the sing-box Android client
and automatically configures IPv4 TCP redirection via su.
This may alleviate the symptoms of some OCD patients who think that
redirect can effectively save power compared to the system HTTP Proxy.
See [Redirect](/configuration/inbound/redirect/).
**2**:
See [Protocol Sniff](/configuration/route/sniff/).
### 1.9.0 ### 1.9.0
* Fixes and improvements * Fixes and improvements
@ -318,7 +482,7 @@ See [Address Filter Fields](/configuration/dns/rule#address-filter-fields).
Important changes since 1.7: Important changes since 1.7:
* Migrate cache file from Clash API to independent options **1** * Migrate cache file from Clash API to independent options **1**
* Introducing [Rule Set](/configuration/rule-set/) **2** * Introducing [rule-set](/configuration/rule-set/) **2**
* Add `sing-box geoip`, `sing-box geosite` and `sing-box rule-set` commands **3** * Add `sing-box geoip`, `sing-box geosite` and `sing-box rule-set` commands **3**
* Allow nested logical rules **4** * Allow nested logical rules **4**
* Independent `source_ip_is_private` and `ip_is_private` rules **5** * Independent `source_ip_is_private` and `ip_is_private` rules **5**
@ -338,7 +502,7 @@ See [Cache File](/configuration/experimental/cache-file/) and
**2**: **2**:
Rule set is independent collections of rules that can be compiled into binaries to improve performance. rule-set is independent collections of rules that can be compiled into binaries to improve performance.
Compared to legacy GeoIP and Geosite resources, Compared to legacy GeoIP and Geosite resources,
it can include more types of rules, load faster, it can include more types of rules, load faster,
use less memory, and update automatically. use less memory, and update automatically.
@ -346,16 +510,16 @@ use less memory, and update automatically.
See [Route#rule_set](/configuration/route/#rule_set), See [Route#rule_set](/configuration/route/#rule_set),
[Route Rule](/configuration/route/rule/), [Route Rule](/configuration/route/rule/),
[DNS Rule](/configuration/dns/rule/), [DNS Rule](/configuration/dns/rule/),
[Rule Set](/configuration/rule-set/), [rule-set](/configuration/rule-set/),
[Source Format](/configuration/rule-set/source-format/) and [Source Format](/configuration/rule-set/source-format/) and
[Headless Rule](/configuration/rule-set/headless-rule/). [Headless Rule](/configuration/rule-set/headless-rule/).
For GEO resources migration, see [Migrate GeoIP to rule sets](/migration/#migrate-geoip-to-rule-sets) and For GEO resources migration, see [Migrate GeoIP to rule-sets](/migration/#migrate-geoip-to-rule-sets) and
[Migrate Geosite to rule sets](/migration/#migrate-geosite-to-rule-sets). [Migrate Geosite to rule-sets](/migration/#migrate-geosite-to-rule-sets).
**3**: **3**:
New commands manage GeoIP, Geosite and rule set resources, and help you migrate GEO resources to rule sets. New commands manage GeoIP, Geosite and rule-set resources, and help you migrate GEO resources to rule-sets.
**4**: **4**:
@ -552,7 +716,7 @@ This change is intended to break incorrect usage and essentially requires no act
**1**: **1**:
Now the rules in the `rule_set` rule item can be logically considered to be merged into the rule using rule sets, Now the rules in the `rule_set` rule item can be logically considered to be merged into the rule using rule-sets,
rather than completely following the AND logic. rather than completely following the AND logic.
#### 1.8.0-alpha.5 #### 1.8.0-alpha.5
@ -568,7 +732,7 @@ Since GeoIP was deprecated, we made this rule independent, see [Migration](/migr
#### 1.8.0-alpha.1 #### 1.8.0-alpha.1
* Migrate cache file from Clash API to independent options **1** * Migrate cache file from Clash API to independent options **1**
* Introducing [Rule Set](/configuration/rule-set/) **2** * Introducing [rule-set](/configuration/rule-set/) **2**
* Add `sing-box geoip`, `sing-box geosite` and `sing-box rule-set` commands **3** * Add `sing-box geoip`, `sing-box geosite` and `sing-box rule-set` commands **3**
* Allow nested logical rules **4** * Allow nested logical rules **4**
@ -579,7 +743,7 @@ See [Cache File](/configuration/experimental/cache-file/) and
**2**: **2**:
Rule set is independent collections of rules that can be compiled into binaries to improve performance. rule-set is independent collections of rules that can be compiled into binaries to improve performance.
Compared to legacy GeoIP and Geosite resources, Compared to legacy GeoIP and Geosite resources,
it can include more types of rules, load faster, it can include more types of rules, load faster,
use less memory, and update automatically. use less memory, and update automatically.
@ -587,16 +751,16 @@ use less memory, and update automatically.
See [Route#rule_set](/configuration/route/#rule_set), See [Route#rule_set](/configuration/route/#rule_set),
[Route Rule](/configuration/route/rule/), [Route Rule](/configuration/route/rule/),
[DNS Rule](/configuration/dns/rule/), [DNS Rule](/configuration/dns/rule/),
[Rule Set](/configuration/rule-set/), [rule-set](/configuration/rule-set/),
[Source Format](/configuration/rule-set/source-format/) and [Source Format](/configuration/rule-set/source-format/) and
[Headless Rule](/configuration/rule-set/headless-rule/). [Headless Rule](/configuration/rule-set/headless-rule/).
For GEO resources migration, see [Migrate GeoIP to rule sets](/migration/#migrate-geoip-to-rule-sets) and For GEO resources migration, see [Migrate GeoIP to rule-sets](/migration/#migrate-geoip-to-rule-sets) and
[Migrate Geosite to rule sets](/migration/#migrate-geosite-to-rule-sets). [Migrate Geosite to rule-sets](/migration/#migrate-geosite-to-rule-sets).
**3**: **3**:
New commands manage GeoIP, Geosite and rule set resources, and help you migrate GEO resources to rule sets. New commands manage GeoIP, Geosite and rule-set resources, and help you migrate GEO resources to rule-sets.
**4**: **4**:

View File

@ -2,6 +2,12 @@
icon: material/new-box icon: material/new-box
--- ---
!!! quote "Changes in sing-box 1.10.0"
:material-delete-clock: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source)
:material-plus: [rule_set_ip_cidr_match_source](#rule_set_ip_cidr_match_source)
:material-plus: [rule_set_ip_cidr_accept_empty](#rule_set_ip_cidr_accept_empty)
!!! quote "Changes in sing-box 1.9.0" !!! quote "Changes in sing-box 1.9.0"
:material-plus: [geoip](#geoip) :material-plus: [geoip](#geoip)
@ -117,7 +123,10 @@ icon: material/new-box
"geoip-cn", "geoip-cn",
"geosite-cn" "geosite-cn"
], ],
// deprecated
"rule_set_ipcidr_match_source": false, "rule_set_ipcidr_match_source": false,
"rule_set_ip_cidr_match_source": false,
"rule_set_ip_cidr_accept_empty": false,
"invert": false, "invert": false,
"outbound": [ "outbound": [
"direct" "direct"
@ -157,7 +166,7 @@ icon: material/new-box
(`source_port` || `source_port_range`) && (`source_port` || `source_port_range`) &&
`other fields` `other fields`
Additionally, included rule sets can be considered merged rather than as a single rule sub-item. Additionally, included rule-sets can be considered merged rather than as a single rule sub-item.
#### inbound #### inbound
@ -303,13 +312,23 @@ Match WiFi BSSID.
!!! question "Since sing-box 1.8.0" !!! question "Since sing-box 1.8.0"
Match [Rule Set](/configuration/route/#rule_set). Match [rule-set](/configuration/route/#rule_set).
#### rule_set_ipcidr_match_source #### rule_set_ipcidr_match_source
!!! question "Since sing-box 1.9.0" !!! question "Since sing-box 1.9.0"
Make `ipcidr` in rule sets match the source IP. !!! failure "Deprecated in sing-box 1.10.0"
`rule_set_ipcidr_match_source` is renamed to `rule_set_ip_cidr_match_source` and will be remove in sing-box 1.11.0.
Make `ip_cidr` rule items in rule-sets match the source IP.
#### rule_set_ip_cidr_match_source
!!! question "Since sing-box 1.10.0"
Make `ip_cidr` rule items in rule-sets match the source IP.
#### invert #### invert
@ -347,11 +366,11 @@ Will overrides `dns.client_subnet` and `servers.[].client_subnet`.
### Address Filter Fields ### Address Filter Fields
Only takes effect for IP address requests. When the query results do not match the address filtering rule items, the current rule will be skipped. Only takes effect for address requests (A/AAAA/HTTPS). When the query results do not match the address filtering rule items, the current rule will be skipped.
!!! info "" !!! info ""
`ip_cidr` items in included rule sets also takes effect as an address filtering field. `ip_cidr` items in included rule-sets also takes effect as an address filtering field.
!!! note "" !!! note ""
@ -375,6 +394,12 @@ Match IP CIDR with query response.
Match private IP with query response. Match private IP with query response.
#### rule_set_ip_cidr_accept_empty
!!! question "Since sing-box 1.10.0"
Make `ip_cidr` rules in rule-sets accept empty query response.
### Logical Fields ### Logical Fields
#### type #### type

View File

@ -2,6 +2,12 @@
icon: material/new-box icon: material/new-box
--- ---
!!! quote "sing-box 1.10.0 中的更改"
:material-delete-clock: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source)
:material-plus: [rule_set_ip_cidr_match_source](#rule_set_ip_cidr_match_source)
:material-plus: [rule_set_ip_cidr_accept_empty](#rule_set_ip_cidr_accept_empty)
!!! quote "sing-box 1.9.0 中的更改" !!! quote "sing-box 1.9.0 中的更改"
:material-plus: [geoip](#geoip) :material-plus: [geoip](#geoip)
@ -117,7 +123,10 @@ icon: material/new-box
"geoip-cn", "geoip-cn",
"geosite-cn" "geosite-cn"
], ],
// 已弃用
"rule_set_ipcidr_match_source": false, "rule_set_ipcidr_match_source": false,
"rule_set_ip_cidr_match_source": false,
"rule_set_ip_cidr_accept_empty": false,
"invert": false, "invert": false,
"outbound": [ "outbound": [
"direct" "direct"
@ -307,7 +316,17 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
!!! question "自 sing-box 1.9.0 起" !!! question "自 sing-box 1.9.0 起"
使规则集中的 `ipcidr` 规则匹配源 IP。 !!! failure "已在 sing-box 1.10.0 废弃"
`rule_set_ipcidr_match_source` 已重命名为 `rule_set_ip_cidr_match_source` 且将在 sing-box 1.11.0 移除。
使规则集中的 `ip_cidr` 规则匹配源 IP。
#### rule_set_ip_cidr_match_source
!!! question "自 sing-box 1.10.0 起"
使规则集中的 `ip_cidr` 规则匹配源 IP。
#### invert #### invert
@ -345,7 +364,7 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
### 地址筛选字段 ### 地址筛选字段
仅对IP地址请求生效。 当查询结果与地址筛选规则项不匹配时,将跳过当前规则。 仅对地址请求 (A/AAAA/HTTPS) 生效。 当查询结果与地址筛选规则项不匹配时,将跳过当前规则。
!!! info "" !!! info ""
@ -365,7 +384,7 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
!!! question "自 sing-box 1.9.0 起" !!! question "自 sing-box 1.9.0 起"
与查询应匹配 IP CIDR。 与查询应匹配 IP CIDR。
#### ip_is_private #### ip_is_private
@ -373,6 +392,12 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
与查询响应匹配非公开 IP。 与查询响应匹配非公开 IP。
#### rule_set_ip_cidr_accept_empty
!!! question "自 sing-box 1.10.0 起"
使规则集中的 `ip_cidr` 规则接受空查询响应。
### 逻辑字段 ### 逻辑字段
#### type #### type

View File

@ -2,6 +2,25 @@
icon: material/new-box icon: material/new-box
--- ---
!!! quote "Changes in sing-box 1.10.0"
:material-plus: [address](#address)
:material-delete-clock: [inet4_address](#inet4_address)
:material-delete-clock: [inet6_address](#inet6_address)
:material-plus: [route_address](#route_address)
:material-delete-clock: [inet4_route_address](#inet4_route_address)
:material-delete-clock: [inet6_route_address](#inet6_route_address)
:material-plus: [route_exclude_address](#route_address)
:material-delete-clock: [inet4_route_exclude_address](#inet4_route_exclude_address)
:material-delete-clock: [inet6_route_exclude_address](#inet6_route_exclude_address)
:material-plus: [iproute2_table_index](#iproute2_table_index)
:material-plus: [iproute2_rule_index](#iproute2_table_index)
:material-plus: [auto_redirect](#auto_redirect)
:material-plus: [auto_redirect_input_mark](#auto_redirect_input_mark)
:material-plus: [auto_redirect_output_mark](#auto_redirect_output_mark)
:material-plus: [route_address_set](#route_address_set)
:material-plus: [route_exclude_address_set](#route_address_set)
!!! quote "Changes in sing-box 1.9.0" !!! quote "Changes in sing-box 1.9.0"
:material-plus: [platform.http_proxy.bypass_domain](#platformhttp_proxybypass_domain) :material-plus: [platform.http_proxy.bypass_domain](#platformhttp_proxybypass_domain)
@ -23,26 +42,61 @@ icon: material/new-box
"type": "tun", "type": "tun",
"tag": "tun-in", "tag": "tun-in",
"interface_name": "tun0", "interface_name": "tun0",
"inet4_address": "172.19.0.1/30", "address": [
"inet6_address": "fdfe:dcba:9876::1/126", "172.18.0.1/30",
"fdfe:dcba:9876::1/126"
],
// deprecated
"inet4_address": [
"172.19.0.1/30"
],
// deprecated
"inet6_address": [
"fdfe:dcba:9876::1/126"
],
"mtu": 9000, "mtu": 9000,
"gso": false, "gso": false,
"auto_route": true, "auto_route": true,
"iproute2_table_index": 2022,
"iproute2_rule_index": 9000,
"auto_redirect": false,
"auto_redirect_input_mark": "0x2023",
"auto_redirect_output_mark": "0x2024",
"strict_route": true, "strict_route": true,
"route_address": [
"0.0.0.0/1",
"128.0.0.0/1",
"::/1",
"8000::/1"
],
// deprecated
"inet4_route_address": [ "inet4_route_address": [
"0.0.0.0/1", "0.0.0.0/1",
"128.0.0.0/1" "128.0.0.0/1"
], ],
// deprecated
"inet6_route_address": [ "inet6_route_address": [
"::/1", "::/1",
"8000::/1" "8000::/1"
], ],
"route_exclude_address": [
"192.168.0.0/16",
"fc00::/7"
],
// deprecated
"inet4_route_exclude_address": [ "inet4_route_exclude_address": [
"192.168.0.0/16" "192.168.0.0/16"
], ],
// deprecated
"inet6_route_exclude_address": [ "inet6_route_exclude_address": [
"fc00::/7" "fc00::/7"
], ],
"route_address_set": [
"geoip-cloudflare"
],
"route_exclude_address_set": [
"geoip-cn"
],
"endpoint_independent_nat": false, "endpoint_independent_nat": false,
"udp_timeout": "5m", "udp_timeout": "5m",
"stack": "system", "stack": "system",
@ -83,8 +137,8 @@ icon: material/new-box
"match_domain": [] "match_domain": []
} }
}, },
...
... // Listen Fields // Listen Fields
} }
``` ```
@ -102,14 +156,26 @@ icon: material/new-box
Virtual device name, automatically selected if empty. Virtual device name, automatically selected if empty.
#### address
!!! question "Since sing-box 1.10.0"
IPv4 and IPv6 prefix for the tun interface.
#### inet4_address #### inet4_address
==Required== !!! failure "Deprecated in sing-box 1.10.0"
`inet4_address` is merged to `address` and will be removed in sing-box 1.11.0.
IPv4 prefix for the tun interface. IPv4 prefix for the tun interface.
#### inet6_address #### inet6_address
!!! failure "Deprecated in sing-box 1.10.0"
`inet6_address` is merged to `address` and will be removed in sing-box 1.11.0.
IPv6 prefix for the tun interface. IPv6 prefix for the tun interface.
#### mtu #### mtu
@ -122,7 +188,7 @@ The maximum transmission unit.
!!! quote "" !!! quote ""
Only supported on Linux. Only supported on Linux with `auto_route` enabled.
Enable generic segmentation offload. Enable generic segmentation offload.
@ -138,6 +204,57 @@ Set the default route to the Tun.
By default, VPN takes precedence over tun. To make tun go through VPN, enable `route.override_android_vpn`. By default, VPN takes precedence over tun. To make tun go through VPN, enable `route.override_android_vpn`.
#### iproute2_table_index
!!! question "Since sing-box 1.10.0"
Linux iproute2 table index generated by `auto_route`.
`2022` is used by default.
#### iproute2_rule_index
!!! question "Since sing-box 1.10.0"
Linux iproute2 rule start index generated by `auto_route`.
`9000` is used by default.
#### auto_redirect
!!! question "Since sing-box 1.10.0"
!!! quote ""
Only supported on Linux with `auto_route` enabled.
Automatically configure iptables/nftables to redirect connections.
*In Android*
Only local connections are forwarded. To share your VPN connection over hotspot or repeater,
use [VPNHotspot](https://github.com/Mygod/VPNHotspot).
*In Linux*:
`auto_route` with `auto_redirect` now works as expected on routers **without intervention**.
#### auto_redirect_input_mark
!!! question "Since sing-box 1.10.0"
Connection input mark used by `route_address_set` and `route_exclude_address_set`.
`0x2023` is used by default.
#### auto_redirect_output_mark
!!! question "Since sing-box 1.10.0"
Connection output mark used by `route_address_set` and `route_exclude_address_set`.
`0x2024` is used by default.
#### strict_route #### strict_route
Enforce strict routing rules when `auto_route` is enabled: Enforce strict routing rules when `auto_route` is enabled:
@ -145,9 +262,10 @@ Enforce strict routing rules when `auto_route` is enabled:
*In Linux*: *In Linux*:
* Let unsupported network unreachable * Let unsupported network unreachable
* Make ICMP traffic route to tun instead of upstream interfaces
* Route all connections to tun * Route all connections to tun
It prevents address leaks and makes DNS hijacking work on Android. It prevents IP address leaks and makes DNS hijacking work on Android.
*In Windows*: *In Windows*:
@ -156,22 +274,80 @@ It prevents address leaks and makes DNS hijacking work on Android.
It may prevent some applications (such as VirtualBox) from working properly in certain situations. It may prevent some applications (such as VirtualBox) from working properly in certain situations.
#### route_address
!!! question "Since sing-box 1.10.0"
Use custom routes instead of default when `auto_route` is enabled.
#### inet4_route_address #### inet4_route_address
!!! failure "Deprecated in sing-box 1.10.0"
`inet4_route_address` is deprecated and will be removed in sing-box 1.11.0, please use [route_address](#route_address)
instead.
Use custom routes instead of default when `auto_route` is enabled. Use custom routes instead of default when `auto_route` is enabled.
#### inet6_route_address #### inet6_route_address
!!! failure "Deprecated in sing-box 1.10.0"
`inet6_route_address` is deprecated and will be removed in sing-box 1.11.0, please use [route_address](#route_address)
instead.
Use custom routes instead of default when `auto_route` is enabled. Use custom routes instead of default when `auto_route` is enabled.
#### route_exclude_address
!!! question "Since sing-box 1.10.0"
Exclude custom routes when `auto_route` is enabled.
#### inet4_route_exclude_address #### inet4_route_exclude_address
!!! failure "Deprecated in sing-box 1.10.0"
`inet4_route_exclude_address` is deprecated and will be removed in sing-box 1.11.0, please
use [route_exclude_address](#route_exclude_address) instead.
Exclude custom routes when `auto_route` is enabled. Exclude custom routes when `auto_route` is enabled.
#### inet6_route_exclude_address #### inet6_route_exclude_address
!!! failure "Deprecated in sing-box 1.10.0"
`inet6_route_exclude_address` is deprecated and will be removed in sing-box 1.11.0, please
use [route_exclude_address](#route_exclude_address) instead.
Exclude custom routes when `auto_route` is enabled. Exclude custom routes when `auto_route` is enabled.
#### route_address_set
!!! question "Since sing-box 1.10.0"
!!! quote ""
Only supported on Linux with nftables and requires `auto_route` and `auto_redirect` enabled.
Add the destination IP CIDR rules in the specified rule-sets to the firewall.
Unmatched traffic will bypass the sing-box routes.
Conflict with `route.default_mark` and `[dialOptions].routing_mark`.
#### route_exclude_address_set
!!! question "Since sing-box 1.10.0"
!!! quote ""
Only supported on Linux with nftables and requires `auto_route` and `auto_redirect` enabled.
Add the destination IP CIDR rules in the specified rule-sets to the firewall.
Matched traffic will bypass the sing-box routes.
Conflict with `route.default_mark` and `[dialOptions].routing_mark`.
#### endpoint_independent_nat #### endpoint_independent_nat
!!! info "" !!! info ""
@ -214,6 +390,10 @@ Conflict with `exclude_interface`.
#### exclude_interface #### exclude_interface
!!! warning ""
When `strict_route` enabled, return traffic to excluded interfaces will not be automatically excluded, so add them as well (example: `br-lan` and `pppoe-wan`).
Exclude interfaces in route. Exclude interfaces in route.
Conflict with `include_interface`. Conflict with `include_interface`.

View File

@ -2,6 +2,25 @@
icon: material/new-box icon: material/new-box
--- ---
!!! quote "Changes in sing-box 1.10.0"
:material-plus: [address](#address)
:material-delete-clock: [inet4_address](#inet4_address)
:material-delete-clock: [inet6_address](#inet6_address)
:material-plus: [route_address](#route_address)
:material-delete-clock: [inet4_route_address](#inet4_route_address)
:material-delete-clock: [inet6_route_address](#inet6_route_address)
:material-plus: [route_exclude_address](#route_address)
:material-delete-clock: [inet4_route_exclude_address](#inet4_route_exclude_address)
:material-delete-clock: [inet6_route_exclude_address](#inet6_route_exclude_address)
:material-plus: [iproute2_table_index](#iproute2_table_index)
:material-plus: [iproute2_rule_index](#iproute2_table_index)
:material-plus: [auto_redirect](#auto_redirect)
:material-plus: [auto_redirect_input_mark](#auto_redirect_input_mark)
:material-plus: [auto_redirect_output_mark](#auto_redirect_output_mark)
:material-plus: [route_address_set](#route_address_set)
:material-plus: [route_exclude_address_set](#route_address_set)
!!! quote "sing-box 1.9.0 中的更改" !!! quote "sing-box 1.9.0 中的更改"
:material-plus: [platform.http_proxy.bypass_domain](#platformhttp_proxybypass_domain) :material-plus: [platform.http_proxy.bypass_domain](#platformhttp_proxybypass_domain)
@ -23,26 +42,61 @@ icon: material/new-box
"type": "tun", "type": "tun",
"tag": "tun-in", "tag": "tun-in",
"interface_name": "tun0", "interface_name": "tun0",
"inet4_address": "172.19.0.1/30", "address": [
"inet6_address": "fdfe:dcba:9876::1/126", "172.18.0.1/30",
"fdfe:dcba:9876::1/126"
],
// 已弃用
"inet4_address": [
"172.19.0.1/30"
],
// 已弃用
"inet6_address": [
"fdfe:dcba:9876::1/126"
],
"mtu": 9000, "mtu": 9000,
"gso": false, "gso": false,
"auto_route": true, "auto_route": true,
"iproute2_table_index": 2022,
"iproute2_rule_index": 9000,
"auto_redirect": false,
"auto_redirect_input_mark": "0x2023",
"auto_redirect_output_mark": "0x2024",
"strict_route": true, "strict_route": true,
"route_address": [
"0.0.0.0/1",
"128.0.0.0/1",
"::/1",
"8000::/1"
],
// 已弃用
"inet4_route_address": [ "inet4_route_address": [
"0.0.0.0/1", "0.0.0.0/1",
"128.0.0.0/1" "128.0.0.0/1"
], ],
// 已弃用
"inet6_route_address": [ "inet6_route_address": [
"::/1", "::/1",
"8000::/1" "8000::/1"
], ],
"route_exclude_address": [
"192.168.0.0/16",
"fc00::/7"
],
// 已弃用
"inet4_route_exclude_address": [ "inet4_route_exclude_address": [
"192.168.0.0/16" "192.168.0.0/16"
], ],
// 已弃用
"inet6_route_exclude_address": [ "inet6_route_exclude_address": [
"fc00::/7" "fc00::/7"
], ],
"route_address_set": [
"geoip-cloudflare"
],
"route_exclude_address_set": [
"geoip-cn"
],
"endpoint_independent_nat": false, "endpoint_independent_nat": false,
"udp_timeout": "5m", "udp_timeout": "5m",
"stack": "system", "stack": "system",
@ -102,14 +156,30 @@ icon: material/new-box
虚拟设备名称,默认自动选择。 虚拟设备名称,默认自动选择。
#### address
!!! question "自 sing-box 1.10.0 起"
==必填==
tun 接口的 IPv4 和 IPv6 前缀。
#### inet4_address #### inet4_address
!!! failure "已在 sing-box 1.10.0 废弃"
`inet4_address` 已合并到 `address` 且将在 sing-box 1.11.0 移除。
==必填== ==必填==
tun 接口的 IPv4 前缀。 tun 接口的 IPv4 前缀。
#### inet6_address #### inet6_address
!!! failure "已在 sing-box 1.10.0 废弃"
`inet6_address` 已合并到 `address` 且将在 sing-box 1.11.0 移除。
tun 接口的 IPv6 前缀。 tun 接口的 IPv6 前缀。
#### mtu #### mtu
@ -138,6 +208,56 @@ tun 接口的 IPv6 前缀。
VPN 默认优先于 tun。要使 tun 经过 VPN启用 `route.override_android_vpn` VPN 默认优先于 tun。要使 tun 经过 VPN启用 `route.override_android_vpn`
#### iproute2_table_index
!!! question "自 sing-box 1.10.0 起"
`auto_route` 生成的 iproute2 路由表索引。
默认使用 `2022`
#### iproute2_rule_index
!!! question "自 sing-box 1.10.0 起"
`auto_route` 生成的 iproute2 规则起始索引。
默认使用 `9000`
#### auto_redirect
!!! question "自 sing-box 1.10.0 起"
!!! quote ""
仅支持 Linux且需要 `auto_route` 已启用。
自动配置 iptables 以重定向 TCP 连接。
*在 Android 中*
仅转发本地 IPv4 连接。 要通过热点或中继共享您的 VPN 连接,请使用 [VPNHotspot](https://github.com/Mygod/VPNHotspot)。
*在 Linux 中*:
带有 `auto_redirect ``auto_route` 现在可以在路由器上按预期工作,**无需干预**。
#### auto_redirect_input_mark
!!! question "自 sing-box 1.10.0 起"
`route_address_set``route_exclude_address_set` 使用的连接输入标记。
默认使用 `0x2023`
#### auto_redirect_output_mark
!!! question "自 sing-box 1.10.0 起"
`route_address_set``route_exclude_address_set` 使用的连接输出标记。
默认使用 `0x2024`
#### strict_route #### strict_route
启用 `auto_route` 时执行严格的路由规则。 启用 `auto_route` 时执行严格的路由规则。
@ -145,9 +265,10 @@ tun 接口的 IPv6 前缀。
*在 Linux 中*: *在 Linux 中*:
* 让不支持的网络无法到达 * 让不支持的网络无法到达
* 使 ICMP 流量路由到 tun 而不是上游接口
* 将所有连接路由到 tun * 将所有连接路由到 tun
它可以防止地址泄漏,并使 DNS 劫持在 Android 上工作。 它可以防止 IP 地址泄漏,并使 DNS 劫持在 Android 上工作。
*在 Windows 中*: *在 Windows 中*:
@ -157,22 +278,76 @@ tun 接口的 IPv6 前缀。
它可能会使某些应用程序(如 VirtualBox在某些情况下无法正常工作。 它可能会使某些应用程序(如 VirtualBox在某些情况下无法正常工作。
#### route_address
!!! question "自 sing-box 1.10.0 起"
设置到 Tun 的自定义路由。
#### inet4_route_address #### inet4_route_address
!!! failure "已在 sing-box 1.10.0 废弃"
`inet4_route_address` 已合并到 `route_address` 且将在 sing-box 1.11.0 移除。
启用 `auto_route` 时使用自定义路由而不是默认路由。 启用 `auto_route` 时使用自定义路由而不是默认路由。
#### inet6_route_address #### inet6_route_address
!!! failure "已在 sing-box 1.10.0 废弃"
`inet6_route_address` 已合并到 `route_address` 且将在 sing-box 1.11.0 移除。
启用 `auto_route` 时使用自定义路由而不是默认路由。 启用 `auto_route` 时使用自定义路由而不是默认路由。
#### route_exclude_address
!!! question "自 sing-box 1.10.0 起"
设置到 Tun 的排除自定义路由。
#### inet4_route_exclude_address #### inet4_route_exclude_address
!!! failure "已在 sing-box 1.10.0 废弃"
`inet4_route_exclude_address` 已合并到 `route_exclude_address` 且将在 sing-box 1.11.0 移除。
启用 `auto_route` 时排除自定义路由。 启用 `auto_route` 时排除自定义路由。
#### inet6_route_exclude_address #### inet6_route_exclude_address
!!! failure "已在 sing-box 1.10.0 废弃"
`inet6_route_exclude_address` 已合并到 `route_exclude_address` 且将在 sing-box 1.11.0 移除。
启用 `auto_route` 时排除自定义路由。 启用 `auto_route` 时排除自定义路由。
#### route_address_set
!!! question "自 sing-box 1.10.0 起"
!!! quote ""
仅支持 Linux且需要 nftables`auto_route``auto_redirect` 已启用。
将指定规则集中的目标 IP CIDR 规则添加到防火墙。
不匹配的流量将绕过 sing-box 路由。
`route.default_mark``[dialOptions].routing_mark` 冲突。
#### route_exclude_address_set
!!! question "自 sing-box 1.10.0 起"
!!! quote ""
仅支持 Linux且需要 nftables`auto_route``auto_redirect` 已启用。
将指定规则集中的目标 IP CIDR 规则添加到防火墙。
匹配的流量将绕过 sing-box 路由。
`route.default_mark``[dialOptions].routing_mark` 冲突。
#### endpoint_independent_nat #### endpoint_independent_nat
启用独立于端点的 NAT。 启用独立于端点的 NAT。
@ -211,6 +386,10 @@ TCP/IP 栈。
#### exclude_interface #### exclude_interface
!!! warning ""
`strict_route` 启用,到被排除接口的回程流量将不会被自动排除,因此也要添加它们(例:`br-lan``pppoe-wan`)。
排除路由的接口。 排除路由的接口。
`include_interface` 冲突。 `include_interface` 冲突。
@ -284,7 +463,7 @@ TCP/IP 栈。
!!! note "" !!! note ""
在 Apple 平台,`bypass_domain` 项匹配主机名 **后缀**. 在 Apple 平台,`bypass_domain` 项匹配主机名 **后缀**.
绕过代理的主机名列表。 绕过代理的主机名列表。

View File

@ -39,7 +39,7 @@ List of [Route Rule](./rule/)
!!! question "Since sing-box 1.8.0" !!! question "Since sing-box 1.8.0"
List of [Rule Set](/configuration/rule-set/) List of [rule-set](/configuration/rule-set/)
#### final #### final

View File

@ -1,3 +1,12 @@
---
icon: material/alert-decagram
---
!!! quote "Changes in sing-box 1.10.0"
:material-delete-clock: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source)
:material-plus: [rule_set_ip_cidr_match_source](#rule_set_ip_cidr_match_source)
!!! quote "Changes in sing-box 1.8.0" !!! quote "Changes in sing-box 1.8.0"
:material-plus: [rule_set](#rule_set) :material-plus: [rule_set](#rule_set)
@ -105,7 +114,9 @@
"geoip-cn", "geoip-cn",
"geosite-cn" "geosite-cn"
], ],
// deprecated
"rule_set_ipcidr_match_source": false, "rule_set_ipcidr_match_source": false,
"rule_set_ip_cidr_match_source": false,
"invert": false, "invert": false,
"outbound": "direct" "outbound": "direct"
}, },
@ -137,7 +148,7 @@
(`source_port` || `source_port_range`) && (`source_port` || `source_port_range`) &&
`other fields` `other fields`
Additionally, included rule sets can be considered merged rather than as a single rule sub-item. Additionally, included rule-sets can be considered merged rather than as a single rule sub-item.
#### inbound #### inbound
@ -297,13 +308,23 @@ Match WiFi BSSID.
!!! question "Since sing-box 1.8.0" !!! question "Since sing-box 1.8.0"
Match [Rule Set](/configuration/route/#rule_set). Match [rule-set](/configuration/route/#rule_set).
#### rule_set_ipcidr_match_source #### rule_set_ipcidr_match_source
!!! question "Since sing-box 1.8.0" !!! question "Since sing-box 1.8.0"
Make `ipcidr` in rule sets match the source IP. !!! failure "Deprecated in sing-box 1.10.0"
`rule_set_ipcidr_match_source` is renamed to `rule_set_ip_cidr_match_source` and will be remove in sing-box 1.11.0.
Make `ip_cidr` in rule-sets match the source IP.
#### rule_set_ip_cidr_match_source
!!! question "Since sing-box 1.10.0"
Make `ip_cidr` in rule-sets match the source IP.
#### invert #### invert

View File

@ -1,3 +1,12 @@
---
icon: material/alert-decagram
---
!!! quote "sing-box 1.10.0 中的更改"
:material-delete-clock: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source)
:material-plus: [rule_set_ip_cidr_match_source](#rule_set_ip_cidr_match_source)
!!! quote "sing-box 1.8.0 中的更改" !!! quote "sing-box 1.8.0 中的更改"
:material-plus: [rule_set](#rule_set) :material-plus: [rule_set](#rule_set)
@ -103,7 +112,9 @@
"geoip-cn", "geoip-cn",
"geosite-cn" "geosite-cn"
], ],
// 已弃用
"rule_set_ipcidr_match_source": false, "rule_set_ipcidr_match_source": false,
"rule_set_ip_cidr_match_source": false,
"invert": false, "invert": false,
"outbound": "direct" "outbound": "direct"
}, },
@ -301,7 +312,17 @@
!!! question "自 sing-box 1.8.0 起" !!! question "自 sing-box 1.8.0 起"
使规则集中的 `ipcidr` 规则匹配源 IP。 !!! failure "已在 sing-box 1.10.0 废弃"
`rule_set_ipcidr_match_source` 已重命名为 `rule_set_ip_cidr_match_source` 且将在 sing-box 1.11.0 移除。
使规则集中的 `ip_cidr` 规则匹配源 IP。
#### rule_set_ip_cidr_match_source
!!! question "自 sing-box 1.10.0 起"
使规则集中的 `ip_cidr` 规则匹配源 IP。
#### invert #### invert

View File

@ -1,11 +1,22 @@
---
icon: material/new-box
---
!!! quote "Changes in sing-box 1.10.0"
:material-plus: BitTorrent support
:material-plus: DTLS support
If enabled in the inbound, the protocol and domain name (if present) of by the connection can be sniffed. If enabled in the inbound, the protocol and domain name (if present) of by the connection can be sniffed.
#### Supported Protocols #### Supported Protocols
| Network | Protocol | Domain Name | | Network | Protocol | Domain Name |
|:-------:|:--------:|:-----------:| |:-------:|:------------:|:-----------:|
| TCP | HTTP | Host | | TCP | `http` | Host |
| TCP | TLS | Server Name | | TCP | `tls` | Server Name |
| UDP | QUIC | Server Name | | UDP | `quic` | Server Name |
| UDP | STUN | / | | UDP | `stun` | / |
| TCP/UDP | DNS | / | | TCP/UDP | `dns` | / |
| TCP/UDP | `bittorrent` | / |
| UDP | `dtls` | / |

View File

@ -1,11 +1,22 @@
---
icon: material/new-box
---
!!! quote "sing-box 1.10.0 中的更改"
:material-plus: BitTorrent 支持
:material-plus: DTLS 支持
如果在入站中启用,则可以嗅探连接的协议和域名(如果存在)。 如果在入站中启用,则可以嗅探连接的协议和域名(如果存在)。
#### 支持的协议 #### 支持的协议
| 网络 | 协议 | 域名 | | 网络 | 协议 | 域名 |
|:-------:|:----:|:-----------:| |:-------:|:------------:|:-----------:|
| TCP | HTTP | Host | | TCP | `http` | Host |
| TCP | TLS | Server Name | | TCP | `tls` | Server Name |
| UDP | QUIC | Server Name | | UDP | `quic` | Server Name |
| UDP | STUN | / | | UDP | `stun` | / |
| TCP/UDP | DNS | / | | TCP/UDP | `dns` | / |
| TCP/UDP | `bittorrent` | / |
| UDP | `dtls` | / |

View File

@ -1,48 +1,56 @@
# Rule Set ---
icon: material/new-box
---
!!! quote "Changes in sing-box 1.10.0"
:material-plus: `type: inline`
# rule-set
!!! question "Since sing-box 1.8.0" !!! question "Since sing-box 1.8.0"
### Structure ### Structure
```json === "Inline"
{
"type": "",
"tag": "",
"format": "",
... // Typed Fields
}
```
#### Local Structure !!! question "Since sing-box 1.10.0"
```json ```json
{ {
"type": "local", "type": "inline", // optional
"tag": "",
... "rules": []
}
"path": "" ```
}
```
#### Remote Structure === "Local File"
!!! info "" ```json
{
"type": "local",
"tag": "",
"format": "source", // or binary
"path": ""
}
```
Remote rule-set will be cached if `experimental.cache_file.enabled`. === "Remote File"
```json !!! info ""
{
"type": "remote", Remote rule-set will be cached if `experimental.cache_file.enabled`.
..., ```json
{
"url": "", "type": "remote",
"download_detour": "", "tag": "",
"update_interval": "" "format": "source", // or binary
} "url": "",
``` "download_detour": "", // optional
"update_interval": "" // optional
}
```
### Fields ### Fields
@ -50,19 +58,31 @@
==Required== ==Required==
Type of Rule Set, `local` or `remote`. Type of rule-set, `local` or `remote`.
#### tag #### tag
==Required== ==Required==
Tag of Rule Set. Tag of rule-set.
### Inline Fields
!!! question "Since sing-box 1.10.0"
#### rules
==Required==
List of [Headless Rule](./headless-rule.md/).
### Local or Remote Fields
#### format #### format
==Required== ==Required==
Format of Rule Set, `source` or `binary`. Format of rule-set file, `source` or `binary`.
### Local Fields ### Local Fields
@ -70,7 +90,11 @@ Format of Rule Set, `source` or `binary`.
==Required== ==Required==
File path of Rule Set. !!! note ""
Will be automatically reloaded if file modified since sing-box 1.10.0.
File path of rule-set.
### Remote Fields ### Remote Fields
@ -78,7 +102,7 @@ File path of Rule Set.
==Required== ==Required==
Download URL of Rule Set. Download URL of rule-set.
#### download_detour #### download_detour
@ -88,6 +112,6 @@ Default outbound will be used if empty.
#### update_interval #### update_interval
Update interval of Rule Set. Update interval of rule-set.
`1d` will be used if empty. `1d` will be used if empty.

View File

@ -21,7 +21,7 @@ Use `sing-box rule-set compile [--output <file-name>.srs] <file-name>.json` to c
==Required== ==Required==
Version of Rule Set, must be `1`. Version of rule-set, must be `1`.
#### rules #### rules

View File

@ -178,6 +178,10 @@ The server certificate line array, in PEM format.
#### certificate_path #### certificate_path
!!! note ""
Will be automatically reloaded if file modified.
The path to the server certificate, in PEM format. The path to the server certificate, in PEM format.
#### key #### key
@ -190,6 +194,10 @@ The server private key line array, in PEM format.
==Server only== ==Server only==
!!! note ""
Will be automatically reloaded if file modified.
The path to the server private key, in PEM format. The path to the server private key, in PEM format.
## Custom TLS support ## Custom TLS support
@ -266,6 +274,10 @@ ECH key line array, in PEM format.
==Server only== ==Server only==
!!! note ""
Will be automatically reloaded if file modified.
The path to ECH key, in PEM format. The path to ECH key, in PEM format.
#### config #### config
@ -397,8 +409,4 @@ A hexadecimal string with zero to eight digits.
The maximum time difference between the server and the client. The maximum time difference between the server and the client.
Check disabled if empty. Check disabled if empty.
### Reload
For server configuration, certificate, key and ECH key will be automatically reloaded if modified.

View File

@ -176,12 +176,20 @@ TLS 版本值:
#### certificate_path #### certificate_path
!!! note ""
文件更改时将自动重新加载。
服务器 PEM 证书路径。 服务器 PEM 证书路径。
#### key #### key
==仅服务器== ==仅服务器==
!!! note ""
文件更改时将自动重新加载。
服务器 PEM 私钥行数组。 服务器 PEM 私钥行数组。
#### key_path #### key_path
@ -258,6 +266,10 @@ ECH PEM 密钥行数组
==仅服务器== ==仅服务器==
!!! note ""
文件更改时将自动重新加载。
ECH PEM 密钥路径 ECH PEM 密钥路径
#### config #### config
@ -384,7 +396,3 @@ ACME DNS01 验证字段。如果配置,将禁用其他验证方法。
服务器与和客户端之间允许的最大时间差。 服务器与和客户端之间允许的最大时间差。
默认禁用检查。 默认禁用检查。
### 重载
对于服务器配置,如果修改,证书和密钥将自动重新加载。

View File

@ -4,6 +4,20 @@ icon: material/delete-alert
# Deprecated Feature List # Deprecated Feature List
## 1.10.0
#### TUN address fields are merged
`inet4_address` and `inet6_address` are merged into `address`,
`inet4_route_address` and `inet6_route_address` are merged into `route_address`,
`inet4_route_exclude_address` and `inet6_route_exclude_address` are merged into `route_exclude_address`.
Old fields are deprecated and will be removed in sing-box 1.11.0.
#### Drop support for go1.18 and go1.19
Due to maintenance difficulties, sing-box 1.10.0 requires at least Go 1.20 to compile.
## 1.8.0 ## 1.8.0
#### Cache file and related features in Clash API #### Cache file and related features in Clash API
@ -19,7 +33,7 @@ The maxmind GeoIP National Database, as an IP classification database,
is not entirely suitable for traffic bypassing, is not entirely suitable for traffic bypassing,
and all existing implementations suffer from high memory usage and difficult management. and all existing implementations suffer from high memory usage and difficult management.
sing-box 1.8.0 introduces [Rule Set](/configuration/rule-set/), which can completely replace GeoIP, sing-box 1.8.0 introduces [rule-set](/configuration/rule-set/), which can completely replace GeoIP,
check [Migration](/migration/#migrate-geoip-to-rule-sets). check [Migration](/migration/#migrate-geoip-to-rule-sets).
#### Geosite #### Geosite
@ -29,7 +43,7 @@ Geosite is deprecated and may be removed in the future.
Geosite, the `domain-list-community` project maintained by V2Ray as an early traffic bypassing solution, Geosite, the `domain-list-community` project maintained by V2Ray as an early traffic bypassing solution,
suffers from a number of problems, including lack of maintenance, inaccurate rules, and difficult management. suffers from a number of problems, including lack of maintenance, inaccurate rules, and difficult management.
sing-box 1.8.0 introduces [Rule Set](/configuration/rule-set/), which can completely replace Geosite, sing-box 1.8.0 introduces [rule-set](/configuration/rule-set/), which can completely replace Geosite,
check [Migration](/migration/#migrate-geosite-to-rule-sets). check [Migration](/migration/#migrate-geosite-to-rule-sets).
## 1.6.0 ## 1.6.0

View File

@ -4,6 +4,20 @@ icon: material/delete-alert
# 废弃功能列表 # 废弃功能列表
## 1.10.0
#### TUN 地址字段已合并
`inet4_address``inet6_address` 已合并为 `address`
`inet4_route_address``inet6_route_address` 已合并为 `route_address`
`inet4_route_exclude_address``inet6_route_exclude_address` 已合并为 `route_exclude_address`
旧字段已废弃,且将在 sing-box 1.11.0 中移除。
#### 移除对 go1.18 和 go1.19 的支持
由于维护困难sing-box 1.10.0 要求至少 Go 1.20 才能编译。
## 1.8.0 ## 1.8.0
#### Clash API 中的 Cache file 及相关功能 #### Clash API 中的 Cache file 及相关功能

View File

@ -2,12 +2,76 @@
icon: material/arrange-bring-forward icon: material/arrange-bring-forward
--- ---
## 1.10.0
### TUN address fields are merged
`inet4_address` and `inet6_address` are merged into `address`,
`inet4_route_address` and `inet6_route_address` are merged into `route_address`,
`inet4_route_exclude_address` and `inet6_route_exclude_address` are merged into `route_exclude_address`.
Old fields are deprecated and will be removed in sing-box 1.11.0.
!!! info "References"
[TUN](/configuration/inbound/tun/)
=== ":material-card-remove: Deprecated"
```json
{
"inbounds": [
{
"type": "tun",
"inet4_address": "172.19.0.1/30",
"inet6_address": "fdfe:dcba:9876::1/126",
"inet4_route_address": [
"0.0.0.0/1",
"128.0.0.0/1"
],
"inet6_route_address": [
"::/1",
"8000::/1"
],
"inet4_route_exclude_address": [
"192.168.0.0/16"
],
"inet6_route_exclude_address": [
"fc00::/7"
]
}
]
}
```
=== ":material-card-multiple: New"
```json
{
"inbounds": [
{
"type": "tun",
"address": [
"172.19.0.1/30",
"fdfe:dcba:9876::1/126"
],
"route_address": [
"0.0.0.0/1",
"128.0.0.0/1",
"::/1",
"8000::/1"
],
"route_exclude_address": [
"192.168.0.0/16",
"fc00::/7"
]
}
]
}
```
## 1.9.0 ## 1.9.0
!!! warning "Unstable"
This version is still under development, and the following migration guide may be changed in the future.
### `domain_suffix` behavior update ### `domain_suffix` behavior update
For historical reasons, sing-box's `domain_suffix` rule matches literal prefixes instead of the same as other projects. For historical reasons, sing-box's `domain_suffix` rule matches literal prefixes instead of the same as other projects.
@ -64,7 +128,7 @@ which will disrupt the existing `process_path` use cases in Windows.
} }
``` ```
### :material-checkbox-intermediate: Migrate GeoIP to rule sets ### :material-checkbox-intermediate: Migrate GeoIP to rule-sets
!!! info "References" !!! info "References"
@ -72,11 +136,11 @@ which will disrupt the existing `process_path` use cases in Windows.
[Route](/configuration/route/) / [Route](/configuration/route/) /
[Route Rule](/configuration/route/rule/) / [Route Rule](/configuration/route/rule/) /
[DNS Rule](/configuration/dns/rule/) / [DNS Rule](/configuration/dns/rule/) /
[Rule Set](/configuration/rule-set/) [rule-set](/configuration/rule-set/)
!!! tip !!! tip
`sing-box geoip` commands can help you convert custom GeoIP into rule sets. `sing-box geoip` commands can help you convert custom GeoIP into rule-sets.
=== ":material-card-remove: Deprecated" === ":material-card-remove: Deprecated"
@ -143,13 +207,13 @@ which will disrupt the existing `process_path` use cases in Windows.
}, },
"experimental": { "experimental": {
"cache_file": { "cache_file": {
"enabled": true // required to save Rule Set cache "enabled": true // required to save rule-set cache
} }
} }
} }
``` ```
### :material-checkbox-intermediate: Migrate Geosite to rule sets ### :material-checkbox-intermediate: Migrate Geosite to rule-sets
!!! info "References" !!! info "References"
@ -157,11 +221,11 @@ which will disrupt the existing `process_path` use cases in Windows.
[Route](/configuration/route/) / [Route](/configuration/route/) /
[Route Rule](/configuration/route/rule/) / [Route Rule](/configuration/route/rule/) /
[DNS Rule](/configuration/dns/rule/) / [DNS Rule](/configuration/dns/rule/) /
[Rule Set](/configuration/rule-set/) [rule-set](/configuration/rule-set/)
!!! tip !!! tip
`sing-box geosite` commands can help you convert custom Geosite into rule sets. `sing-box geosite` commands can help you convert custom Geosite into rule-sets.
=== ":material-card-remove: Deprecated" === ":material-card-remove: Deprecated"
@ -204,7 +268,7 @@ which will disrupt the existing `process_path` use cases in Windows.
}, },
"experimental": { "experimental": {
"cache_file": { "cache_file": {
"enabled": true // required to save Rule Set cache "enabled": true // required to save rule-set cache
} }
} }
} }

View File

@ -2,12 +2,76 @@
icon: material/arrange-bring-forward icon: material/arrange-bring-forward
--- ---
## 1.10.0
### TUN 地址字段已合并
`inet4_address``inet6_address` 已合并为 `address`
`inet4_route_address``inet6_route_address` 已合并为 `route_address`
`inet4_route_exclude_address``inet6_route_exclude_address` 已合并为 `route_exclude_address`
旧字段已废弃,且将在 sing-box 1.11.0 中移除。
!!! info "参考"
[TUN](/zh/configuration/inbound/tun/)
=== ":material-card-remove: 弃用的"
```json
{
"inbounds": [
{
"type": "tun",
"inet4_address": "172.19.0.1/30",
"inet6_address": "fdfe:dcba:9876::1/126",
"inet4_route_address": [
"0.0.0.0/1",
"128.0.0.0/1"
],
"inet6_route_address": [
"::/1",
"8000::/1"
],
"inet4_route_exclude_address": [
"192.168.0.0/16"
],
"inet6_route_exclude_address": [
"fc00::/7"
]
}
]
}
```
=== ":material-card-multiple: 新的"
```json
{
"inbounds": [
{
"type": "tun",
"address": [
"172.19.0.1/30",
"fdfe:dcba:9876::1/126"
],
"route_address": [
"0.0.0.0/1",
"128.0.0.0/1",
"::/1",
"8000::/1"
],
"route_exclude_address": [
"192.168.0.0/16",
"fc00::/7"
]
}
]
}
```
## 1.9.0 ## 1.9.0
!!! warning "不稳定的"
该版本仍在开发中,迁移指南可能将在未来更改。
### `domain_suffix` 行为更新 ### `domain_suffix` 行为更新
由于历史原因sing-box 的 `domain_suffix` 规则匹配字面前缀,而不与其他项目相同。 由于历史原因sing-box 的 `domain_suffix` 规则匹配字面前缀,而不与其他项目相同。
@ -142,7 +206,7 @@ sing-box 1.9.0 使 QueryFullProcessImageNameW 输出 Win32 路径(如 `C:\fold
}, },
"experimental": { "experimental": {
"cache_file": { "cache_file": {
"enabled": true // required to save Rule Set cache "enabled": true // required to save rule-set cache
} }
} }
} }
@ -203,7 +267,7 @@ sing-box 1.9.0 使 QueryFullProcessImageNameW 输出 Win32 路径(如 `C:\fold
}, },
"experimental": { "experimental": {
"cache_file": { "cache_file": {
"enabled": true // required to save Rule Set cache "enabled": true // required to save rule-set cache
} }
} }
} }

View File

@ -14,6 +14,7 @@ import (
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/go-chi/render" "github.com/go-chi/render"
"github.com/gofrs/uuid/v5"
) )
func connectionRouter(router adapter.Router, trafficManager *trafficontrol.Manager) http.Handler { func connectionRouter(router adapter.Router, trafficManager *trafficontrol.Manager) http.Handler {
@ -76,10 +77,10 @@ func getConnections(trafficManager *trafficontrol.Manager) func(w http.ResponseW
func closeConnection(trafficManager *trafficontrol.Manager) func(w http.ResponseWriter, r *http.Request) { func closeConnection(trafficManager *trafficontrol.Manager) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id") id := uuid.FromStringOrNil(chi.URLParam(r, "id"))
snapshot := trafficManager.Snapshot() snapshot := trafficManager.Snapshot()
for _, c := range snapshot.Connections { for _, c := range snapshot.Connections {
if id == c.ID() { if id == c.Metadata().ID {
c.Close() c.Close()
break break
} }

View File

@ -7,7 +7,9 @@ import (
"net" "net"
"net/http" "net/http"
"os" "os"
"runtime"
"strings" "strings"
"syscall"
"time" "time"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
@ -19,7 +21,6 @@ import (
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/json"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/service" "github.com/sagernet/sing/service"
@ -144,7 +145,18 @@ func (s *Server) PreStart() error {
func (s *Server) Start() error { func (s *Server) Start() error {
if s.externalController { if s.externalController {
s.checkAndDownloadExternalUI() s.checkAndDownloadExternalUI()
listener, err := net.Listen("tcp", s.httpServer.Addr) var (
listener net.Listener
err error
)
for i := 0; i < 3; i++ {
listener, err = net.Listen("tcp", s.httpServer.Addr)
if runtime.GOOS == "android" && errors.Is(err, syscall.EADDRINUSE) {
time.Sleep(100 * time.Millisecond)
continue
}
break
}
if err != nil { if err != nil {
return E.Cause(err, "external controller listen error") return E.Cause(err, "external controller listen error")
} }
@ -218,58 +230,15 @@ func (s *Server) TrafficManager() *trafficontrol.Manager {
} }
func (s *Server) RoutedConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, matchedRule adapter.Rule) (net.Conn, adapter.Tracker) { func (s *Server) RoutedConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, matchedRule adapter.Rule) (net.Conn, adapter.Tracker) {
tracker := trafficontrol.NewTCPTracker(conn, s.trafficManager, castMetadata(metadata), s.router, matchedRule) tracker := trafficontrol.NewTCPTracker(conn, s.trafficManager, metadata, s.router, matchedRule)
return tracker, tracker return tracker, tracker
} }
func (s *Server) RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, matchedRule adapter.Rule) (N.PacketConn, adapter.Tracker) { func (s *Server) RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, matchedRule adapter.Rule) (N.PacketConn, adapter.Tracker) {
tracker := trafficontrol.NewUDPTracker(conn, s.trafficManager, castMetadata(metadata), s.router, matchedRule) tracker := trafficontrol.NewUDPTracker(conn, s.trafficManager, metadata, s.router, matchedRule)
return tracker, tracker return tracker, tracker
} }
func castMetadata(metadata adapter.InboundContext) trafficontrol.Metadata {
var inbound string
if metadata.Inbound != "" {
inbound = metadata.InboundType + "/" + metadata.Inbound
} else {
inbound = metadata.InboundType
}
var domain string
if metadata.Domain != "" {
domain = metadata.Domain
} else {
domain = metadata.Destination.Fqdn
}
var processPath string
if metadata.ProcessInfo != nil {
if metadata.ProcessInfo.ProcessPath != "" {
processPath = metadata.ProcessInfo.ProcessPath
} else if metadata.ProcessInfo.PackageName != "" {
processPath = metadata.ProcessInfo.PackageName
}
if processPath == "" {
if metadata.ProcessInfo.UserId != -1 {
processPath = F.ToString(metadata.ProcessInfo.UserId)
}
} else if metadata.ProcessInfo.User != "" {
processPath = F.ToString(processPath, " (", metadata.ProcessInfo.User, ")")
} else if metadata.ProcessInfo.UserId != -1 {
processPath = F.ToString(processPath, " (", metadata.ProcessInfo.UserId, ")")
}
}
return trafficontrol.Metadata{
NetWork: metadata.Network,
Type: inbound,
SrcIP: metadata.Source.Addr,
DstIP: metadata.Destination.Addr,
SrcPort: F.ToString(metadata.Source.Port),
DstPort: F.ToString(metadata.Destination.Port),
Host: domain,
DNSMode: "normal",
ProcessPath: processPath,
}
}
func authentication(serverSecret string) func(next http.Handler) http.Handler { func authentication(serverSecret string) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) { fn := func(w http.ResponseWriter, r *http.Request) {

View File

@ -2,10 +2,17 @@ package trafficontrol
import ( import (
"runtime" "runtime"
"sync"
"time" "time"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/experimental/clashapi/compatible" "github.com/sagernet/sing-box/experimental/clashapi/compatible"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/atomic" "github.com/sagernet/sing/common/atomic"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/x/list"
"github.com/gofrs/uuid/v5"
) )
type Manager struct { type Manager struct {
@ -16,9 +23,11 @@ type Manager struct {
uploadTotal atomic.Int64 uploadTotal atomic.Int64
downloadTotal atomic.Int64 downloadTotal atomic.Int64
connections compatible.Map[string, tracker] connections compatible.Map[uuid.UUID, Tracker]
ticker *time.Ticker closedConnectionsAccess sync.Mutex
done chan struct{} closedConnections list.List[TrackerMetadata]
ticker *time.Ticker
done chan struct{}
// process *process.Process // process *process.Process
memory uint64 memory uint64
} }
@ -33,12 +42,22 @@ func NewManager() *Manager {
return manager return manager
} }
func (m *Manager) Join(c tracker) { func (m *Manager) Join(c Tracker) {
m.connections.Store(c.ID(), c) m.connections.Store(c.Metadata().ID, c)
} }
func (m *Manager) Leave(c tracker) { func (m *Manager) Leave(c Tracker) {
m.connections.Delete(c.ID()) metadata := c.Metadata()
_, loaded := m.connections.LoadAndDelete(metadata.ID)
if loaded {
metadata.ClosedAt = time.Now()
m.closedConnectionsAccess.Lock()
defer m.closedConnectionsAccess.Unlock()
if m.closedConnections.Len() >= 1000 {
m.closedConnections.PopFront()
}
m.closedConnections.PushBack(metadata)
}
} }
func (m *Manager) PushUploaded(size int64) { func (m *Manager) PushUploaded(size int64) {
@ -59,14 +78,39 @@ func (m *Manager) Total() (up int64, down int64) {
return m.uploadTotal.Load(), m.downloadTotal.Load() return m.uploadTotal.Load(), m.downloadTotal.Load()
} }
func (m *Manager) Connections() int { func (m *Manager) ConnectionsLen() int {
return m.connections.Len() return m.connections.Len()
} }
func (m *Manager) Connections() []TrackerMetadata {
var connections []TrackerMetadata
m.connections.Range(func(_ uuid.UUID, value Tracker) bool {
connections = append(connections, value.Metadata())
return true
})
return connections
}
func (m *Manager) ClosedConnections() []TrackerMetadata {
m.closedConnectionsAccess.Lock()
defer m.closedConnectionsAccess.Unlock()
return m.closedConnections.Array()
}
func (m *Manager) Connection(id uuid.UUID) Tracker {
connection, loaded := m.connections.Load(id)
if !loaded {
return nil
}
return connection
}
func (m *Manager) Snapshot() *Snapshot { func (m *Manager) Snapshot() *Snapshot {
var connections []tracker var connections []Tracker
m.connections.Range(func(_ string, value tracker) bool { m.connections.Range(func(_ uuid.UUID, value Tracker) bool {
connections = append(connections, value) if value.Metadata().OutboundType != C.TypeDNS {
connections = append(connections, value)
}
return true return true
}) })
@ -75,10 +119,10 @@ func (m *Manager) Snapshot() *Snapshot {
m.memory = memStats.StackInuse + memStats.HeapInuse + memStats.HeapIdle - memStats.HeapReleased m.memory = memStats.StackInuse + memStats.HeapInuse + memStats.HeapIdle - memStats.HeapReleased
return &Snapshot{ return &Snapshot{
UploadTotal: m.uploadTotal.Load(), Upload: m.uploadTotal.Load(),
DownloadTotal: m.downloadTotal.Load(), Download: m.downloadTotal.Load(),
Connections: connections, Connections: connections,
Memory: m.memory, Memory: m.memory,
} }
} }
@ -114,8 +158,17 @@ func (m *Manager) Close() error {
} }
type Snapshot struct { type Snapshot struct {
DownloadTotal int64 `json:"downloadTotal"` Download int64
UploadTotal int64 `json:"uploadTotal"` Upload int64
Connections []tracker `json:"connections"` Connections []Tracker
Memory uint64 `json:"memory"` Memory uint64
}
func (s *Snapshot) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]any{
"downloadTotal": s.Download,
"uploadTotal": s.Upload,
"connections": common.Map(s.Connections, func(t Tracker) TrackerMetadata { return t.Metadata() }),
"memory": s.Memory,
})
} }

View File

@ -2,97 +2,135 @@ package trafficontrol
import ( import (
"net" "net"
"net/netip"
"time" "time"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/atomic" "github.com/sagernet/sing/common/atomic"
"github.com/sagernet/sing/common/bufio" "github.com/sagernet/sing/common/bufio"
F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/json"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
"github.com/gofrs/uuid/v5" "github.com/gofrs/uuid/v5"
) )
type Metadata struct { type TrackerMetadata struct {
NetWork string `json:"network"` ID uuid.UUID
Type string `json:"type"` Metadata adapter.InboundContext
SrcIP netip.Addr `json:"sourceIP"` CreatedAt time.Time
DstIP netip.Addr `json:"destinationIP"` ClosedAt time.Time
SrcPort string `json:"sourcePort"` Upload *atomic.Int64
DstPort string `json:"destinationPort"` Download *atomic.Int64
Host string `json:"host"` Chain []string
DNSMode string `json:"dnsMode"` Rule adapter.Rule
ProcessPath string `json:"processPath"` Outbound string
OutboundType string
} }
type tracker interface { func (t TrackerMetadata) MarshalJSON() ([]byte, error) {
ID() string var inbound string
Close() error if t.Metadata.Inbound != "" {
Leave() inbound = t.Metadata.InboundType + "/" + t.Metadata.Inbound
} } else {
inbound = t.Metadata.InboundType
type trackerInfo struct { }
UUID uuid.UUID `json:"id"` var domain string
Metadata Metadata `json:"metadata"` if t.Metadata.Domain != "" {
UploadTotal *atomic.Int64 `json:"upload"` domain = t.Metadata.Domain
DownloadTotal *atomic.Int64 `json:"download"` } else {
Start time.Time `json:"start"` domain = t.Metadata.Destination.Fqdn
Chain []string `json:"chains"` }
Rule string `json:"rule"` var processPath string
RulePayload string `json:"rulePayload"` if t.Metadata.ProcessInfo != nil {
} if t.Metadata.ProcessInfo.ProcessPath != "" {
processPath = t.Metadata.ProcessInfo.ProcessPath
func (t trackerInfo) MarshalJSON() ([]byte, error) { } else if t.Metadata.ProcessInfo.PackageName != "" {
processPath = t.Metadata.ProcessInfo.PackageName
}
if processPath == "" {
if t.Metadata.ProcessInfo.UserId != -1 {
processPath = F.ToString(t.Metadata.ProcessInfo.UserId)
}
} else if t.Metadata.ProcessInfo.User != "" {
processPath = F.ToString(processPath, " (", t.Metadata.ProcessInfo.User, ")")
} else if t.Metadata.ProcessInfo.UserId != -1 {
processPath = F.ToString(processPath, " (", t.Metadata.ProcessInfo.UserId, ")")
}
}
var rule string
if t.Rule != nil {
rule = F.ToString(t.Rule, " => ", t.Rule.Outbound())
} else {
rule = "final"
}
return json.Marshal(map[string]any{ return json.Marshal(map[string]any{
"id": t.UUID.String(), "id": t.ID,
"metadata": t.Metadata, "metadata": map[string]any{
"upload": t.UploadTotal.Load(), "network": t.Metadata.Network,
"download": t.DownloadTotal.Load(), "type": inbound,
"start": t.Start, "sourceIP": t.Metadata.Source.Addr,
"destinationIP": t.Metadata.Destination.Addr,
"sourcePort": F.ToString(t.Metadata.Source.Port),
"destinationPort": F.ToString(t.Metadata.Destination.Port),
"host": domain,
"dnsMode": "normal",
"processPath": processPath,
},
"upload": t.Upload.Load(),
"download": t.Download.Load(),
"start": t.CreatedAt,
"chains": t.Chain, "chains": t.Chain,
"rule": t.Rule, "rule": rule,
"rulePayload": t.RulePayload, "rulePayload": "",
}) })
} }
type tcpTracker struct { type Tracker interface {
N.ExtendedConn `json:"-"` adapter.Tracker
*trackerInfo Metadata() TrackerMetadata
manager *Manager Close() error
} }
func (tt *tcpTracker) ID() string { type TCPConn struct {
return tt.UUID.String() N.ExtendedConn
metadata TrackerMetadata
manager *Manager
} }
func (tt *tcpTracker) Close() error { func (tt *TCPConn) Metadata() TrackerMetadata {
return tt.metadata
}
func (tt *TCPConn) Close() error {
tt.manager.Leave(tt) tt.manager.Leave(tt)
return tt.ExtendedConn.Close() return tt.ExtendedConn.Close()
} }
func (tt *tcpTracker) Leave() { func (tt *TCPConn) Leave() {
tt.manager.Leave(tt) tt.manager.Leave(tt)
} }
func (tt *tcpTracker) Upstream() any { func (tt *TCPConn) Upstream() any {
return tt.ExtendedConn return tt.ExtendedConn
} }
func (tt *tcpTracker) ReaderReplaceable() bool { func (tt *TCPConn) ReaderReplaceable() bool {
return true return true
} }
func (tt *tcpTracker) WriterReplaceable() bool { func (tt *TCPConn) WriterReplaceable() bool {
return true return true
} }
func NewTCPTracker(conn net.Conn, manager *Manager, metadata Metadata, router adapter.Router, rule adapter.Rule) *tcpTracker { func NewTCPTracker(conn net.Conn, manager *Manager, metadata adapter.InboundContext, router adapter.Router, rule adapter.Rule) *TCPConn {
uuid, _ := uuid.NewV4() id, _ := uuid.NewV4()
var (
var chain []string chain []string
var next string next string
outbound string
outboundType string
)
if rule == nil { if rule == nil {
if defaultOutbound, err := router.DefaultOutbound(N.NetworkTCP); err == nil { if defaultOutbound, err := router.DefaultOutbound(N.NetworkTCP); err == nil {
next = defaultOutbound.Tag() next = defaultOutbound.Tag()
@ -106,17 +144,17 @@ func NewTCPTracker(conn net.Conn, manager *Manager, metadata Metadata, router ad
if !loaded { if !loaded {
break break
} }
outbound = detour.Tag()
outboundType = detour.Type()
group, isGroup := detour.(adapter.OutboundGroup) group, isGroup := detour.(adapter.OutboundGroup)
if !isGroup { if !isGroup {
break break
} }
next = group.Now() next = group.Now()
} }
upload := new(atomic.Int64) upload := new(atomic.Int64)
download := new(atomic.Int64) download := new(atomic.Int64)
tracker := &TCPConn{
t := &tcpTracker{
ExtendedConn: bufio.NewCounterConn(conn, []N.CountFunc{func(n int64) { ExtendedConn: bufio.NewCounterConn(conn, []N.CountFunc{func(n int64) {
upload.Add(n) upload.Add(n)
manager.PushUploaded(n) manager.PushUploaded(n)
@ -124,64 +162,62 @@ func NewTCPTracker(conn net.Conn, manager *Manager, metadata Metadata, router ad
download.Add(n) download.Add(n)
manager.PushDownloaded(n) manager.PushDownloaded(n)
}}), }}),
manager: manager, metadata: TrackerMetadata{
trackerInfo: &trackerInfo{ ID: id,
UUID: uuid, Metadata: metadata,
Start: time.Now(), CreatedAt: time.Now(),
Metadata: metadata, Upload: upload,
Chain: common.Reverse(chain), Download: download,
Rule: "", Chain: common.Reverse(chain),
UploadTotal: upload, Rule: rule,
DownloadTotal: download, Outbound: outbound,
OutboundType: outboundType,
}, },
manager: manager,
} }
manager.Join(tracker)
if rule != nil { return tracker
t.trackerInfo.Rule = rule.String() + " => " + rule.Outbound()
} else {
t.trackerInfo.Rule = "final"
}
manager.Join(t)
return t
} }
type udpTracker struct { type UDPConn struct {
N.PacketConn `json:"-"` N.PacketConn `json:"-"`
*trackerInfo metadata TrackerMetadata
manager *Manager manager *Manager
} }
func (ut *udpTracker) ID() string { func (ut *UDPConn) Metadata() TrackerMetadata {
return ut.UUID.String() return ut.metadata
} }
func (ut *udpTracker) Close() error { func (ut *UDPConn) Close() error {
ut.manager.Leave(ut) ut.manager.Leave(ut)
return ut.PacketConn.Close() return ut.PacketConn.Close()
} }
func (ut *udpTracker) Leave() { func (ut *UDPConn) Leave() {
ut.manager.Leave(ut) ut.manager.Leave(ut)
} }
func (ut *udpTracker) Upstream() any { func (ut *UDPConn) Upstream() any {
return ut.PacketConn return ut.PacketConn
} }
func (ut *udpTracker) ReaderReplaceable() bool { func (ut *UDPConn) ReaderReplaceable() bool {
return true return true
} }
func (ut *udpTracker) WriterReplaceable() bool { func (ut *UDPConn) WriterReplaceable() bool {
return true return true
} }
func NewUDPTracker(conn N.PacketConn, manager *Manager, metadata Metadata, router adapter.Router, rule adapter.Rule) *udpTracker { func NewUDPTracker(conn N.PacketConn, manager *Manager, metadata adapter.InboundContext, router adapter.Router, rule adapter.Rule) *UDPConn {
uuid, _ := uuid.NewV4() id, _ := uuid.NewV4()
var (
var chain []string chain []string
var next string next string
outbound string
outboundType string
)
if rule == nil { if rule == nil {
if defaultOutbound, err := router.DefaultOutbound(N.NetworkUDP); err == nil { if defaultOutbound, err := router.DefaultOutbound(N.NetworkUDP); err == nil {
next = defaultOutbound.Tag() next = defaultOutbound.Tag()
@ -195,17 +231,17 @@ func NewUDPTracker(conn N.PacketConn, manager *Manager, metadata Metadata, route
if !loaded { if !loaded {
break break
} }
outbound = detour.Tag()
outboundType = detour.Type()
group, isGroup := detour.(adapter.OutboundGroup) group, isGroup := detour.(adapter.OutboundGroup)
if !isGroup { if !isGroup {
break break
} }
next = group.Now() next = group.Now()
} }
upload := new(atomic.Int64) upload := new(atomic.Int64)
download := new(atomic.Int64) download := new(atomic.Int64)
trackerConn := &UDPConn{
ut := &udpTracker{
PacketConn: bufio.NewCounterPacketConn(conn, []N.CountFunc{func(n int64) { PacketConn: bufio.NewCounterPacketConn(conn, []N.CountFunc{func(n int64) {
upload.Add(n) upload.Add(n)
manager.PushUploaded(n) manager.PushUploaded(n)
@ -213,24 +249,19 @@ func NewUDPTracker(conn N.PacketConn, manager *Manager, metadata Metadata, route
download.Add(n) download.Add(n)
manager.PushDownloaded(n) manager.PushDownloaded(n)
}}), }}),
manager: manager, metadata: TrackerMetadata{
trackerInfo: &trackerInfo{ ID: id,
UUID: uuid, Metadata: metadata,
Start: time.Now(), CreatedAt: time.Now(),
Metadata: metadata, Upload: upload,
Chain: common.Reverse(chain), Download: download,
Rule: "", Chain: common.Reverse(chain),
UploadTotal: upload, Rule: rule,
DownloadTotal: download, Outbound: outbound,
OutboundType: outboundType,
}, },
manager: manager,
} }
manager.Join(trackerConn)
if rule != nil { return trackerConn
ut.trackerInfo.Rule = rule.String() + " => " + rule.Outbound()
} else {
ut.trackerInfo.Rule = "final"
}
manager.Join(ut)
return ut
} }

View File

@ -14,4 +14,6 @@ const (
CommandSetClashMode CommandSetClashMode
CommandGetSystemProxyStatus CommandGetSystemProxyStatus
CommandSetSystemProxyEnabled CommandSetSystemProxyEnabled
CommandConnections
CommandCloseConnection
) )

View File

@ -9,7 +9,7 @@ import (
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/experimental/clashapi" "github.com/sagernet/sing-box/experimental/clashapi"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/rw" "github.com/sagernet/sing/common/varbin"
) )
func (c *CommandClient) SetClashMode(newMode string) error { func (c *CommandClient) SetClashMode(newMode string) error {
@ -22,7 +22,7 @@ func (c *CommandClient) SetClashMode(newMode string) error {
if err != nil { if err != nil {
return err return err
} }
err = rw.WriteVString(conn, newMode) err = varbin.Write(conn, binary.BigEndian, newMode)
if err != nil { if err != nil {
return err return err
} }
@ -30,7 +30,7 @@ func (c *CommandClient) SetClashMode(newMode string) error {
} }
func (s *CommandServer) handleSetClashMode(conn net.Conn) error { func (s *CommandServer) handleSetClashMode(conn net.Conn) error {
newMode, err := rw.ReadVString(conn) newMode, err := varbin.ReadValue[string](conn, binary.BigEndian)
if err != nil { if err != nil {
return err return err
} }
@ -50,7 +50,7 @@ func (c *CommandClient) handleModeConn(conn net.Conn) {
defer conn.Close() defer conn.Close()
for { for {
newMode, err := rw.ReadVString(conn) newMode, err := varbin.ReadValue[string](conn, binary.BigEndian)
if err != nil { if err != nil {
c.handler.Disconnected(err.Error()) c.handler.Disconnected(err.Error())
return return
@ -80,7 +80,7 @@ func (s *CommandServer) handleModeConn(conn net.Conn) error {
for { for {
select { select {
case <-s.modeUpdate: case <-s.modeUpdate:
err = rw.WriteVString(conn, clashServer.Mode()) err = varbin.Write(conn, binary.BigEndian, clashServer.Mode())
if err != nil { if err != nil {
return err return err
} }
@ -101,12 +101,12 @@ func readClashModeList(reader io.Reader) (modeList []string, currentMode string,
} }
modeList = make([]string, modeListLength) modeList = make([]string, modeListLength)
for i := 0; i < int(modeListLength); i++ { for i := 0; i < int(modeListLength); i++ {
modeList[i], err = rw.ReadVString(reader) modeList[i], err = varbin.ReadValue[string](reader, binary.BigEndian)
if err != nil { if err != nil {
return return
} }
} }
currentMode, err = rw.ReadVString(reader) currentMode, err = varbin.ReadValue[string](reader, binary.BigEndian)
return return
} }
@ -118,12 +118,12 @@ func writeClashModeList(writer io.Writer, clashServer adapter.ClashServer) error
} }
if len(modeList) > 0 { if len(modeList) > 0 {
for _, mode := range modeList { for _, mode := range modeList {
err = rw.WriteVString(writer, mode) err = varbin.Write(writer, binary.BigEndian, mode)
if err != nil { if err != nil {
return err return err
} }
} }
err = rw.WriteVString(writer, clashServer.Mode()) err = varbin.Write(writer, binary.BigEndian, clashServer.Mode())
if err != nil { if err != nil {
return err return err
} }

View File

@ -25,12 +25,13 @@ type CommandClientOptions struct {
type CommandClientHandler interface { type CommandClientHandler interface {
Connected() Connected()
Disconnected(message string) Disconnected(message string)
ClearLog() ClearLogs()
WriteLog(message string) WriteLogs(messageList StringIterator)
WriteStatus(message *StatusMessage) WriteStatus(message *StatusMessage)
WriteGroups(message OutboundGroupIterator) WriteGroups(message OutboundGroupIterator)
InitializeClashMode(modeList StringIterator, currentMode string) InitializeClashMode(modeList StringIterator, currentMode string)
UpdateClashMode(newMode string) UpdateClashMode(newMode string)
WriteConnections(message *Connections)
} }
func NewStandaloneCommandClient() *CommandClient { func NewStandaloneCommandClient() *CommandClient {
@ -83,6 +84,10 @@ func (c *CommandClient) Connect() error {
} }
switch c.options.Command { switch c.options.Command {
case CommandLog: case CommandLog:
err = binary.Write(conn, binary.BigEndian, c.options.StatusInterval)
if err != nil {
return E.Cause(err, "write interval")
}
c.handler.Connected() c.handler.Connected()
go c.handleLogConn(conn) go c.handleLogConn(conn)
case CommandStatus: case CommandStatus:
@ -116,6 +121,13 @@ func (c *CommandClient) Connect() error {
return nil return nil
} }
go c.handleModeConn(conn) go c.handleModeConn(conn)
case CommandConnections:
err = binary.Write(conn, binary.BigEndian, c.options.StatusInterval)
if err != nil {
return E.Cause(err, "write interval")
}
c.handler.Connected()
go c.handleConnectionsConn(conn)
} }
return nil return nil
} }

View File

@ -0,0 +1,54 @@
package libbox
import (
"bufio"
"net"
"github.com/sagernet/sing-box/experimental/clashapi"
"github.com/sagernet/sing/common/binary"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/varbin"
"github.com/gofrs/uuid/v5"
)
func (c *CommandClient) CloseConnection(connId string) error {
conn, err := c.directConnect()
if err != nil {
return err
}
defer conn.Close()
writer := bufio.NewWriter(conn)
err = varbin.Write(writer, binary.BigEndian, connId)
if err != nil {
return err
}
err = writer.Flush()
if err != nil {
return err
}
return readError(conn)
}
func (s *CommandServer) handleCloseConnection(conn net.Conn) error {
reader := bufio.NewReader(conn)
var connId string
err := varbin.Read(reader, binary.BigEndian, &connId)
if err != nil {
return E.Cause(err, "read connection id")
}
service := s.service
if service == nil {
return writeError(conn, E.New("service not ready"))
}
clashServer := service.instance.Router().ClashServer()
if clashServer == nil {
return writeError(conn, E.New("Clash API disabled"))
}
targetConn := clashServer.(*clashapi.Server).TrafficManager().Connection(uuid.FromStringOrNil(connId))
if targetConn == nil {
return writeError(conn, E.New("connection already closed"))
}
targetConn.Close()
return writeError(conn, nil)
}

View File

@ -0,0 +1,272 @@
package libbox
import (
"bufio"
"net"
"slices"
"strings"
"time"
"github.com/sagernet/sing-box/experimental/clashapi"
"github.com/sagernet/sing-box/experimental/clashapi/trafficontrol"
"github.com/sagernet/sing/common/binary"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/common/varbin"
"github.com/gofrs/uuid/v5"
)
func (c *CommandClient) handleConnectionsConn(conn net.Conn) {
defer conn.Close()
reader := bufio.NewReader(conn)
var (
rawConnections []Connection
connections Connections
)
for {
err := varbin.Read(reader, binary.BigEndian, &rawConnections)
if err != nil {
c.handler.Disconnected(err.Error())
return
}
connections.input = rawConnections
c.handler.WriteConnections(&connections)
}
}
func (s *CommandServer) handleConnectionsConn(conn net.Conn) error {
var interval int64
err := binary.Read(conn, binary.BigEndian, &interval)
if err != nil {
return E.Cause(err, "read interval")
}
ticker := time.NewTicker(time.Duration(interval))
defer ticker.Stop()
ctx := connKeepAlive(conn)
var trafficManager *trafficontrol.Manager
for {
service := s.service
if service != nil {
clashServer := service.instance.Router().ClashServer()
if clashServer == nil {
return E.New("Clash API disabled")
}
trafficManager = clashServer.(*clashapi.Server).TrafficManager()
break
}
select {
case <-ctx.Done():
return ctx.Err()
case <-ticker.C:
}
}
var (
connections = make(map[uuid.UUID]*Connection)
outConnections []Connection
)
writer := bufio.NewWriter(conn)
for {
outConnections = outConnections[:0]
for _, connection := range trafficManager.Connections() {
outConnections = append(outConnections, newConnection(connections, connection, false))
}
for _, connection := range trafficManager.ClosedConnections() {
outConnections = append(outConnections, newConnection(connections, connection, true))
}
err = varbin.Write(writer, binary.BigEndian, outConnections)
if err != nil {
return err
}
err = writer.Flush()
if err != nil {
return err
}
select {
case <-ctx.Done():
return ctx.Err()
case <-ticker.C:
}
}
}
const (
ConnectionStateAll = iota
ConnectionStateActive
ConnectionStateClosed
)
type Connections struct {
input []Connection
filtered []Connection
}
func (c *Connections) FilterState(state int32) {
c.filtered = c.filtered[:0]
switch state {
case ConnectionStateAll:
c.filtered = append(c.filtered, c.input...)
case ConnectionStateActive:
for _, connection := range c.input {
if connection.ClosedAt == 0 {
c.filtered = append(c.filtered, connection)
}
}
case ConnectionStateClosed:
for _, connection := range c.input {
if connection.ClosedAt != 0 {
c.filtered = append(c.filtered, connection)
}
}
}
}
func (c *Connections) SortByDate() {
slices.SortStableFunc(c.filtered, func(x, y Connection) int {
if x.CreatedAt < y.CreatedAt {
return 1
} else if x.CreatedAt > y.CreatedAt {
return -1
} else {
return strings.Compare(y.ID, x.ID)
}
})
}
func (c *Connections) SortByTraffic() {
slices.SortStableFunc(c.filtered, func(x, y Connection) int {
xTraffic := x.Uplink + x.Downlink
yTraffic := y.Uplink + y.Downlink
if xTraffic < yTraffic {
return 1
} else if xTraffic > yTraffic {
return -1
} else {
return strings.Compare(y.ID, x.ID)
}
})
}
func (c *Connections) SortByTrafficTotal() {
slices.SortStableFunc(c.filtered, func(x, y Connection) int {
xTraffic := x.UplinkTotal + x.DownlinkTotal
yTraffic := y.UplinkTotal + y.DownlinkTotal
if xTraffic < yTraffic {
return 1
} else if xTraffic > yTraffic {
return -1
} else {
return strings.Compare(y.ID, x.ID)
}
})
}
func (c *Connections) Iterator() ConnectionIterator {
return newPtrIterator(c.filtered)
}
type Connection struct {
ID string
Inbound string
InboundType string
IPVersion int32
Network string
Source string
Destination string
Domain string
Protocol string
User string
FromOutbound string
CreatedAt int64
ClosedAt int64
Uplink int64
Downlink int64
UplinkTotal int64
DownlinkTotal int64
Rule string
Outbound string
OutboundType string
ChainList []string
}
func (c *Connection) Chain() StringIterator {
return newIterator(c.ChainList)
}
func (c *Connection) DisplayDestination() string {
destination := M.ParseSocksaddr(c.Destination)
if destination.IsIP() && c.Domain != "" {
destination = M.Socksaddr{
Fqdn: c.Domain,
Port: destination.Port,
}
return destination.String()
}
return c.Destination
}
type ConnectionIterator interface {
Next() *Connection
HasNext() bool
}
func newConnection(connections map[uuid.UUID]*Connection, metadata trafficontrol.TrackerMetadata, isClosed bool) Connection {
if oldConnection, loaded := connections[metadata.ID]; loaded {
if isClosed {
if oldConnection.ClosedAt == 0 {
oldConnection.Uplink = 0
oldConnection.Downlink = 0
oldConnection.ClosedAt = metadata.ClosedAt.UnixMilli()
}
return *oldConnection
}
lastUplink := oldConnection.UplinkTotal
lastDownlink := oldConnection.DownlinkTotal
uplinkTotal := metadata.Upload.Load()
downlinkTotal := metadata.Download.Load()
oldConnection.Uplink = uplinkTotal - lastUplink
oldConnection.Downlink = downlinkTotal - lastDownlink
oldConnection.UplinkTotal = uplinkTotal
oldConnection.DownlinkTotal = downlinkTotal
return *oldConnection
}
var rule string
if metadata.Rule != nil {
rule = metadata.Rule.String()
}
uplinkTotal := metadata.Upload.Load()
downlinkTotal := metadata.Download.Load()
uplink := uplinkTotal
downlink := downlinkTotal
var closedAt int64
if !metadata.ClosedAt.IsZero() {
closedAt = metadata.ClosedAt.UnixMilli()
uplink = 0
downlink = 0
}
connection := Connection{
ID: metadata.ID.String(),
Inbound: metadata.Metadata.Inbound,
InboundType: metadata.Metadata.InboundType,
IPVersion: int32(metadata.Metadata.IPVersion),
Network: metadata.Metadata.Network,
Source: metadata.Metadata.Source.String(),
Destination: metadata.Metadata.Destination.String(),
Domain: metadata.Metadata.Domain,
Protocol: metadata.Metadata.Protocol,
User: metadata.Metadata.User,
FromOutbound: metadata.Metadata.Outbound,
CreatedAt: metadata.CreatedAt.UnixMilli(),
ClosedAt: closedAt,
Uplink: uplink,
Downlink: downlink,
UplinkTotal: uplinkTotal,
DownlinkTotal: downlinkTotal,
Rule: rule,
Outbound: metadata.Outbound,
OutboundType: metadata.OutboundType,
ChainList: metadata.Chain,
}
connections[metadata.ID] = &connection
return connection
}

View File

@ -1,6 +1,7 @@
package libbox package libbox
import ( import (
"bufio"
"encoding/binary" "encoding/binary"
"io" "io"
"net" "net"
@ -10,40 +11,10 @@ import (
"github.com/sagernet/sing-box/common/urltest" "github.com/sagernet/sing-box/common/urltest"
"github.com/sagernet/sing-box/outbound" "github.com/sagernet/sing-box/outbound"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/rw" "github.com/sagernet/sing/common/varbin"
"github.com/sagernet/sing/service" "github.com/sagernet/sing/service"
) )
type OutboundGroup struct {
Tag string
Type string
Selectable bool
Selected string
IsExpand bool
items []*OutboundGroupItem
}
func (g *OutboundGroup) GetItems() OutboundGroupItemIterator {
return newIterator(g.items)
}
type OutboundGroupIterator interface {
Next() *OutboundGroup
HasNext() bool
}
type OutboundGroupItem struct {
Tag string
Type string
URLTestTime int64
URLTestDelay int32
}
type OutboundGroupItemIterator interface {
Next() *OutboundGroupItem
HasNext() bool
}
func (c *CommandClient) handleGroupConn(conn net.Conn) { func (c *CommandClient) handleGroupConn(conn net.Conn) {
defer conn.Close() defer conn.Close()
@ -66,19 +37,24 @@ func (s *CommandServer) handleGroupConn(conn net.Conn) error {
ticker := time.NewTicker(time.Duration(interval)) ticker := time.NewTicker(time.Duration(interval))
defer ticker.Stop() defer ticker.Stop()
ctx := connKeepAlive(conn) ctx := connKeepAlive(conn)
writer := bufio.NewWriter(conn)
for { for {
service := s.service service := s.service
if service != nil { if service != nil {
err := writeGroups(conn, service) err = writeGroups(writer, service)
if err != nil { if err != nil {
return err return err
} }
} else { } else {
err := binary.Write(conn, binary.BigEndian, uint16(0)) err = binary.Write(writer, binary.BigEndian, uint16(0))
if err != nil { if err != nil {
return err return err
} }
} }
err = writer.Flush()
if err != nil {
return err
}
select { select {
case <-ctx.Done(): case <-ctx.Done():
return ctx.Err() return ctx.Err()
@ -92,74 +68,41 @@ func (s *CommandServer) handleGroupConn(conn net.Conn) error {
} }
} }
type OutboundGroup struct {
Tag string
Type string
Selectable bool
Selected string
IsExpand bool
ItemList []*OutboundGroupItem
}
func (g *OutboundGroup) GetItems() OutboundGroupItemIterator {
return newIterator(g.ItemList)
}
type OutboundGroupIterator interface {
Next() *OutboundGroup
HasNext() bool
}
type OutboundGroupItem struct {
Tag string
Type string
URLTestTime int64
URLTestDelay int32
}
type OutboundGroupItemIterator interface {
Next() *OutboundGroupItem
HasNext() bool
}
func readGroups(reader io.Reader) (OutboundGroupIterator, error) { func readGroups(reader io.Reader) (OutboundGroupIterator, error) {
var groupLength uint16 groups, err := varbin.ReadValue[[]*OutboundGroup](reader, binary.BigEndian)
err := binary.Read(reader, binary.BigEndian, &groupLength)
if err != nil { if err != nil {
return nil, err return nil, err
} }
groups := make([]*OutboundGroup, 0, groupLength)
for i := 0; i < int(groupLength); i++ {
var group OutboundGroup
group.Tag, err = rw.ReadVString(reader)
if err != nil {
return nil, err
}
group.Type, err = rw.ReadVString(reader)
if err != nil {
return nil, err
}
err = binary.Read(reader, binary.BigEndian, &group.Selectable)
if err != nil {
return nil, err
}
group.Selected, err = rw.ReadVString(reader)
if err != nil {
return nil, err
}
err = binary.Read(reader, binary.BigEndian, &group.IsExpand)
if err != nil {
return nil, err
}
var itemLength uint16
err = binary.Read(reader, binary.BigEndian, &itemLength)
if err != nil {
return nil, err
}
group.items = make([]*OutboundGroupItem, itemLength)
for j := 0; j < int(itemLength); j++ {
var item OutboundGroupItem
item.Tag, err = rw.ReadVString(reader)
if err != nil {
return nil, err
}
item.Type, err = rw.ReadVString(reader)
if err != nil {
return nil, err
}
err = binary.Read(reader, binary.BigEndian, &item.URLTestTime)
if err != nil {
return nil, err
}
err = binary.Read(reader, binary.BigEndian, &item.URLTestDelay)
if err != nil {
return nil, err
}
group.items[j] = &item
}
groups = append(groups, &group)
}
return newIterator(groups), nil return newIterator(groups), nil
} }
@ -199,63 +142,14 @@ func writeGroups(writer io.Writer, boxService *BoxService) error {
item.URLTestTime = history.Time.Unix() item.URLTestTime = history.Time.Unix()
item.URLTestDelay = int32(history.Delay) item.URLTestDelay = int32(history.Delay)
} }
group.items = append(group.items, &item) group.ItemList = append(group.ItemList, &item)
} }
if len(group.items) < 2 { if len(group.ItemList) < 2 {
continue continue
} }
groups = append(groups, group) groups = append(groups, group)
} }
return varbin.Write(writer, binary.BigEndian, groups)
err := binary.Write(writer, binary.BigEndian, uint16(len(groups)))
if err != nil {
return err
}
for _, group := range groups {
err = rw.WriteVString(writer, group.Tag)
if err != nil {
return err
}
err = rw.WriteVString(writer, group.Type)
if err != nil {
return err
}
err = binary.Write(writer, binary.BigEndian, group.Selectable)
if err != nil {
return err
}
err = rw.WriteVString(writer, group.Selected)
if err != nil {
return err
}
err = binary.Write(writer, binary.BigEndian, group.IsExpand)
if err != nil {
return err
}
err = binary.Write(writer, binary.BigEndian, uint16(len(group.items)))
if err != nil {
return err
}
for _, item := range group.items {
err = rw.WriteVString(writer, item.Tag)
if err != nil {
return err
}
err = rw.WriteVString(writer, item.Type)
if err != nil {
return err
}
err = binary.Write(writer, binary.BigEndian, item.URLTestTime)
if err != nil {
return err
}
err = binary.Write(writer, binary.BigEndian, item.URLTestDelay)
if err != nil {
return err
}
}
}
return nil
} }
func (c *CommandClient) SetGroupExpand(groupTag string, isExpand bool) error { func (c *CommandClient) SetGroupExpand(groupTag string, isExpand bool) error {
@ -268,7 +162,7 @@ func (c *CommandClient) SetGroupExpand(groupTag string, isExpand bool) error {
if err != nil { if err != nil {
return err return err
} }
err = rw.WriteVString(conn, groupTag) err = varbin.Write(conn, binary.BigEndian, groupTag)
if err != nil { if err != nil {
return err return err
} }
@ -280,7 +174,7 @@ func (c *CommandClient) SetGroupExpand(groupTag string, isExpand bool) error {
} }
func (s *CommandServer) handleSetGroupExpand(conn net.Conn) error { func (s *CommandServer) handleSetGroupExpand(conn net.Conn) error {
groupTag, err := rw.ReadVString(conn) groupTag, err := varbin.ReadValue[string](conn, binary.BigEndian)
if err != nil { if err != nil {
return err return err
} }

View File

@ -1,12 +1,27 @@
package libbox package libbox
import ( import (
"bufio"
"context" "context"
"encoding/binary"
"io" "io"
"net" "net"
"time"
"github.com/sagernet/sing/common/binary"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/varbin"
) )
func (s *CommandServer) ResetLog() {
s.access.Lock()
defer s.access.Unlock()
s.savedLines.Init()
select {
case s.logReset <- struct{}{}:
default:
}
}
func (s *CommandServer) WriteMessage(message string) { func (s *CommandServer) WriteMessage(message string) {
s.subscriber.Emit(message) s.subscriber.Emit(message)
s.access.Lock() s.access.Lock()
@ -17,43 +32,19 @@ func (s *CommandServer) WriteMessage(message string) {
s.access.Unlock() s.access.Unlock()
} }
func readLog(reader io.Reader) ([]byte, error) {
var messageLength uint16
err := binary.Read(reader, binary.BigEndian, &messageLength)
if err != nil {
return nil, err
}
if messageLength == 0 {
return nil, nil
}
data := make([]byte, messageLength)
_, err = io.ReadFull(reader, data)
if err != nil {
return nil, err
}
return data, nil
}
func writeLog(writer io.Writer, message []byte) error {
err := binary.Write(writer, binary.BigEndian, uint8(0))
if err != nil {
return err
}
err = binary.Write(writer, binary.BigEndian, uint16(len(message)))
if err != nil {
return err
}
if len(message) > 0 {
_, err = writer.Write(message)
}
return err
}
func writeClearLog(writer io.Writer) error {
return binary.Write(writer, binary.BigEndian, uint8(1))
}
func (s *CommandServer) handleLogConn(conn net.Conn) error { func (s *CommandServer) handleLogConn(conn net.Conn) error {
var (
interval int64
timer *time.Timer
)
err := binary.Read(conn, binary.BigEndian, &interval)
if err != nil {
return E.Cause(err, "read interval")
}
timer = time.NewTimer(time.Duration(interval))
if !timer.Stop() {
<-timer.C
}
var savedLines []string var savedLines []string
s.access.Lock() s.access.Lock()
savedLines = make([]string, 0, s.savedLines.Len()) savedLines = make([]string, 0, s.savedLines.Len())
@ -66,52 +57,90 @@ func (s *CommandServer) handleLogConn(conn net.Conn) error {
return err return err
} }
defer s.observer.UnSubscribe(subscription) defer s.observer.UnSubscribe(subscription)
for _, line := range savedLines { writer := bufio.NewWriter(conn)
err = writeLog(conn, []byte(line)) select {
case <-s.logReset:
err = writer.WriteByte(1)
if err != nil {
return err
}
err = writer.Flush()
if err != nil {
return err
}
default:
}
if len(savedLines) > 0 {
err = writer.WriteByte(0)
if err != nil {
return err
}
err = varbin.Write(writer, binary.BigEndian, savedLines)
if err != nil { if err != nil {
return err return err
} }
} }
ctx := connKeepAlive(conn) ctx := connKeepAlive(conn)
var logLines []string
for { for {
err = writer.Flush()
if err != nil {
return err
}
select { select {
case <-ctx.Done(): case <-ctx.Done():
return ctx.Err() return ctx.Err()
case message := <-subscription:
err = writeLog(conn, []byte(message))
if err != nil {
return err
}
case <-s.logReset: case <-s.logReset:
err = writeClearLog(conn) err = writer.WriteByte(1)
if err != nil { if err != nil {
return err return err
} }
case <-done: case <-done:
return nil return nil
case logLine := <-subscription:
logLines = logLines[:0]
logLines = append(logLines, logLine)
timer.Reset(time.Duration(interval))
loopLogs:
for {
select {
case logLine = <-subscription:
logLines = append(logLines, logLine)
case <-timer.C:
break loopLogs
}
}
err = writer.WriteByte(0)
if err != nil {
return err
}
err = varbin.Write(writer, binary.BigEndian, logLines)
if err != nil {
return err
}
} }
} }
} }
func (c *CommandClient) handleLogConn(conn net.Conn) { func (c *CommandClient) handleLogConn(conn net.Conn) {
reader := bufio.NewReader(conn)
for { for {
var messageType uint8 messageType, err := reader.ReadByte()
err := binary.Read(conn, binary.BigEndian, &messageType)
if err != nil { if err != nil {
c.handler.Disconnected(err.Error()) c.handler.Disconnected(err.Error())
return return
} }
var message []byte var messages []string
switch messageType { switch messageType {
case 0: case 0:
message, err = readLog(conn) err = varbin.Read(reader, binary.BigEndian, &messages)
if err != nil { if err != nil {
c.handler.Disconnected(err.Error()) c.handler.Disconnected(err.Error())
return return
} }
c.handler.WriteLog(string(message)) c.handler.WriteLogs(newIterator(messages))
case 1: case 1:
c.handler.ClearLog() c.handler.ClearLogs()
} }
} }
} }
@ -120,7 +149,7 @@ func connKeepAlive(reader io.Reader) context.Context {
ctx, cancel := context.WithCancelCause(context.Background()) ctx, cancel := context.WithCancelCause(context.Background())
go func() { go func() {
for { for {
_, err := readLog(reader) _, err := reader.Read(make([]byte, 1))
if err != nil { if err != nil {
cancel(err) cancel(err)
return return

View File

@ -5,7 +5,7 @@ import (
"net" "net"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/rw" "github.com/sagernet/sing/common/varbin"
) )
func (c *CommandClient) ServiceReload() error { func (c *CommandClient) ServiceReload() error {
@ -24,7 +24,7 @@ func (c *CommandClient) ServiceReload() error {
return err return err
} }
if hasError { if hasError {
errorMessage, err := rw.ReadVString(conn) errorMessage, err := varbin.ReadValue[string](conn, binary.BigEndian)
if err != nil { if err != nil {
return err return err
} }
@ -40,7 +40,7 @@ func (s *CommandServer) handleServiceReload(conn net.Conn) error {
return err return err
} }
if rErr != nil { if rErr != nil {
return rw.WriteVString(conn, rErr.Error()) return varbin.Write(conn, binary.BigEndian, rErr.Error())
} }
return nil return nil
} }
@ -61,7 +61,7 @@ func (c *CommandClient) ServiceClose() error {
return nil return nil
} }
if hasError { if hasError {
errorMessage, err := rw.ReadVString(conn) errorMessage, err := varbin.ReadValue[string](conn, binary.BigEndian)
if err != nil { if err != nil {
return nil return nil
} }
@ -78,7 +78,7 @@ func (s *CommandServer) handleServiceClose(conn net.Conn) error {
return err return err
} }
if rErr != nil { if rErr != nil {
return rw.WriteVString(conn, rErr.Error()) return varbin.Write(conn, binary.BigEndian, rErr.Error())
} }
return nil return nil
} }

View File

@ -6,7 +6,7 @@ import (
"github.com/sagernet/sing-box/outbound" "github.com/sagernet/sing-box/outbound"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/rw" "github.com/sagernet/sing/common/varbin"
) )
func (c *CommandClient) SelectOutbound(groupTag string, outboundTag string) error { func (c *CommandClient) SelectOutbound(groupTag string, outboundTag string) error {
@ -19,11 +19,11 @@ func (c *CommandClient) SelectOutbound(groupTag string, outboundTag string) erro
if err != nil { if err != nil {
return err return err
} }
err = rw.WriteVString(conn, groupTag) err = varbin.Write(conn, binary.BigEndian, groupTag)
if err != nil { if err != nil {
return err return err
} }
err = rw.WriteVString(conn, outboundTag) err = varbin.Write(conn, binary.BigEndian, outboundTag)
if err != nil { if err != nil {
return err return err
} }
@ -31,11 +31,11 @@ func (c *CommandClient) SelectOutbound(groupTag string, outboundTag string) erro
} }
func (s *CommandServer) handleSelectOutbound(conn net.Conn) error { func (s *CommandServer) handleSelectOutbound(conn net.Conn) error {
groupTag, err := rw.ReadVString(conn) groupTag, err := varbin.ReadValue[string](conn, binary.BigEndian)
if err != nil { if err != nil {
return err return err
} }
outboundTag, err := rw.ReadVString(conn) outboundTag, err := varbin.ReadValue[string](conn, binary.BigEndian)
if err != nil { if err != nil {
return err return err
} }

View File

@ -33,6 +33,8 @@ type CommandServer struct {
urlTestUpdate chan struct{} urlTestUpdate chan struct{}
modeUpdate chan struct{} modeUpdate chan struct{}
logReset chan struct{} logReset chan struct{}
closedConnections []Connection
} }
type CommandServerHandler interface { type CommandServerHandler interface {
@ -64,14 +66,6 @@ func (s *CommandServer) SetService(newService *BoxService) {
s.notifyURLTestUpdate() s.notifyURLTestUpdate()
} }
func (s *CommandServer) ResetLog() {
s.savedLines.Init()
select {
case s.logReset <- struct{}{}:
default:
}
}
func (s *CommandServer) notifyURLTestUpdate() { func (s *CommandServer) notifyURLTestUpdate() {
select { select {
case s.urlTestUpdate <- struct{}{}: case s.urlTestUpdate <- struct{}{}:
@ -176,6 +170,10 @@ func (s *CommandServer) handleConnection(conn net.Conn) error {
return s.handleGetSystemProxyStatus(conn) return s.handleGetSystemProxyStatus(conn)
case CommandSetSystemProxyEnabled: case CommandSetSystemProxyEnabled:
return s.handleSetSystemProxyEnabled(conn) return s.handleSetSystemProxyEnabled(conn)
case CommandConnections:
return s.handleConnectionsConn(conn)
case CommandCloseConnection:
return s.handleCloseConnection(conn)
default: default:
return E.New("unknown command: ", command) return E.New("unknown command: ", command)
} }

View File

@ -5,7 +5,7 @@ import (
"io" "io"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/rw" "github.com/sagernet/sing/common/varbin"
) )
func readError(reader io.Reader) error { func readError(reader io.Reader) error {
@ -15,7 +15,7 @@ func readError(reader io.Reader) error {
return err return err
} }
if hasError { if hasError {
errorMessage, err := rw.ReadVString(reader) errorMessage, err := varbin.ReadValue[string](reader, binary.BigEndian)
if err != nil { if err != nil {
return err return err
} }
@ -30,7 +30,7 @@ func writeError(writer io.Writer, wErr error) error {
return err return err
} }
if wErr != nil { if wErr != nil {
err = rw.WriteVString(writer, wErr.Error()) err = varbin.Write(writer, binary.BigEndian, wErr.Error())
if err != nil { if err != nil {
return err return err
} }

View File

@ -36,7 +36,7 @@ func (s *CommandServer) readStatus() StatusMessage {
trafficManager := clashServer.(*clashapi.Server).TrafficManager() trafficManager := clashServer.(*clashapi.Server).TrafficManager()
message.Uplink, message.Downlink = trafficManager.Now() message.Uplink, message.Downlink = trafficManager.Now()
message.UplinkTotal, message.DownlinkTotal = trafficManager.Total() message.UplinkTotal, message.DownlinkTotal = trafficManager.Total()
message.ConnectionsIn = int32(trafficManager.Connections()) message.ConnectionsIn = int32(trafficManager.ConnectionsLen())
} }
} }

View File

@ -11,7 +11,7 @@ import (
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/batch" "github.com/sagernet/sing/common/batch"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/rw" "github.com/sagernet/sing/common/varbin"
"github.com/sagernet/sing/service" "github.com/sagernet/sing/service"
) )
@ -25,7 +25,7 @@ func (c *CommandClient) URLTest(groupTag string) error {
if err != nil { if err != nil {
return err return err
} }
err = rw.WriteVString(conn, groupTag) err = varbin.Write(conn, binary.BigEndian, groupTag)
if err != nil { if err != nil {
return err return err
} }
@ -33,7 +33,7 @@ func (c *CommandClient) URLTest(groupTag string) error {
} }
func (s *CommandServer) handleURLTest(conn net.Conn) error { func (s *CommandServer) handleURLTest(conn net.Conn) error {
groupTag, err := rw.ReadVString(conn) groupTag, err := varbin.ReadValue[string](conn, binary.BigEndian)
if err != nil { if err != nil {
return err return err
} }

View File

@ -3,8 +3,9 @@ package libbox
import "github.com/sagernet/sing/common" import "github.com/sagernet/sing/common"
type StringIterator interface { type StringIterator interface {
Next() string Len() int32
HasNext() bool HasNext() bool
Next() string
} }
var _ StringIterator = (*iterator[string])(nil) var _ StringIterator = (*iterator[string])(nil)
@ -17,6 +18,18 @@ func newIterator[T any](values []T) *iterator[T] {
return &iterator[T]{values} return &iterator[T]{values}
} }
func newPtrIterator[T any](values []T) *iterator[*T] {
return &iterator[*T]{common.Map(values, func(value T) *T { return &value })}
}
func (i *iterator[T]) Len() int32 {
return int32(len(i.values))
}
func (i *iterator[T]) HasNext() bool {
return len(i.values) > 0
}
func (i *iterator[T]) Next() T { func (i *iterator[T]) Next() T {
if len(i.values) == 0 { if len(i.values) == 0 {
return common.DefaultValue[T]() return common.DefaultValue[T]()
@ -26,10 +39,6 @@ func (i *iterator[T]) Next() T {
return nextValue return nextValue
} }
func (i *iterator[T]) HasNext() bool {
return len(i.values) > 0
}
type abstractIterator[T any] interface { type abstractIterator[T any] interface {
Next() T Next() T
HasNext() bool HasNext() bool

View File

@ -1,13 +1,13 @@
package libbox package libbox
import ( import (
"bufio"
"bytes" "bytes"
"compress/gzip" "compress/gzip"
"encoding/binary" "encoding/binary"
"io"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/rw" "github.com/sagernet/sing/common/varbin"
) )
func EncodeChunkedMessage(data []byte) []byte { func EncodeChunkedMessage(data []byte) []byte {
@ -35,13 +35,13 @@ type ErrorMessage struct {
func (e *ErrorMessage) Encode() []byte { func (e *ErrorMessage) Encode() []byte {
var buffer bytes.Buffer var buffer bytes.Buffer
buffer.WriteByte(MessageTypeError) buffer.WriteByte(MessageTypeError)
rw.WriteVString(&buffer, e.Message) varbin.Write(&buffer, binary.BigEndian, e.Message)
return buffer.Bytes() return buffer.Bytes()
} }
func DecodeErrorMessage(data []byte) (*ErrorMessage, error) { func DecodeErrorMessage(data []byte) (*ErrorMessage, error) {
reader := bytes.NewReader(data) reader := bytes.NewReader(data)
messageType, err := rw.ReadByte(reader) messageType, err := reader.ReadByte()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -49,7 +49,7 @@ func DecodeErrorMessage(data []byte) (*ErrorMessage, error) {
return nil, E.New("invalid message") return nil, E.New("invalid message")
} }
var message ErrorMessage var message ErrorMessage
message.Message, err = rw.ReadVString(reader) message.Message, err = varbin.ReadValue[string](reader, binary.BigEndian)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -87,7 +87,7 @@ func (e *ProfileEncoder) Encode() []byte {
binary.Write(&buffer, binary.BigEndian, uint16(len(e.profiles))) binary.Write(&buffer, binary.BigEndian, uint16(len(e.profiles)))
for _, preview := range e.profiles { for _, preview := range e.profiles {
binary.Write(&buffer, binary.BigEndian, preview.ProfileID) binary.Write(&buffer, binary.BigEndian, preview.ProfileID)
rw.WriteVString(&buffer, preview.Name) varbin.Write(&buffer, binary.BigEndian, preview.Name)
binary.Write(&buffer, binary.BigEndian, preview.Type) binary.Write(&buffer, binary.BigEndian, preview.Type)
} }
return buffer.Bytes() return buffer.Bytes()
@ -117,7 +117,7 @@ func (d *ProfileDecoder) Decode(data []byte) error {
if err != nil { if err != nil {
return err return err
} }
profile.Name, err = rw.ReadVString(reader) profile.Name, err = varbin.ReadValue[string](reader, binary.BigEndian)
if err != nil { if err != nil {
return err return err
} }
@ -147,7 +147,7 @@ func (r *ProfileContentRequest) Encode() []byte {
func DecodeProfileContentRequest(data []byte) (*ProfileContentRequest, error) { func DecodeProfileContentRequest(data []byte) (*ProfileContentRequest, error) {
reader := bytes.NewReader(data) reader := bytes.NewReader(data)
messageType, err := rw.ReadByte(reader) messageType, err := reader.ReadByte()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -176,12 +176,13 @@ func (c *ProfileContent) Encode() []byte {
buffer := new(bytes.Buffer) buffer := new(bytes.Buffer)
buffer.WriteByte(MessageTypeProfileContent) buffer.WriteByte(MessageTypeProfileContent)
buffer.WriteByte(1) buffer.WriteByte(1)
writer := gzip.NewWriter(buffer) gWriter := gzip.NewWriter(buffer)
rw.WriteVString(writer, c.Name) writer := bufio.NewWriter(gWriter)
varbin.Write(writer, binary.BigEndian, c.Name)
binary.Write(writer, binary.BigEndian, c.Type) binary.Write(writer, binary.BigEndian, c.Type)
rw.WriteVString(writer, c.Config) varbin.Write(writer, binary.BigEndian, c.Config)
if c.Type != ProfileTypeLocal { if c.Type != ProfileTypeLocal {
rw.WriteVString(writer, c.RemotePath) varbin.Write(writer, binary.BigEndian, c.RemotePath)
} }
if c.Type == ProfileTypeRemote { if c.Type == ProfileTypeRemote {
binary.Write(writer, binary.BigEndian, c.AutoUpdate) binary.Write(writer, binary.BigEndian, c.AutoUpdate)
@ -189,29 +190,31 @@ func (c *ProfileContent) Encode() []byte {
binary.Write(writer, binary.BigEndian, c.LastUpdated) binary.Write(writer, binary.BigEndian, c.LastUpdated)
} }
writer.Flush() writer.Flush()
writer.Close() gWriter.Flush()
gWriter.Close()
return buffer.Bytes() return buffer.Bytes()
} }
func DecodeProfileContent(data []byte) (*ProfileContent, error) { func DecodeProfileContent(data []byte) (*ProfileContent, error) {
var reader io.Reader = bytes.NewReader(data) reader := bytes.NewReader(data)
messageType, err := rw.ReadByte(reader) messageType, err := reader.ReadByte()
if err != nil { if err != nil {
return nil, err return nil, err
} }
if messageType != MessageTypeProfileContent { if messageType != MessageTypeProfileContent {
return nil, E.New("invalid message") return nil, E.New("invalid message")
} }
version, err := rw.ReadByte(reader) version, err := reader.ReadByte()
if err != nil { if err != nil {
return nil, err return nil, err
} }
reader, err = gzip.NewReader(reader) gReader, err := gzip.NewReader(reader)
if err != nil { if err != nil {
return nil, E.Cause(err, "unsupported profile") return nil, E.Cause(err, "unsupported profile")
} }
bReader := varbin.StubReader(gReader)
var content ProfileContent var content ProfileContent
content.Name, err = rw.ReadVString(reader) content.Name, err = varbin.ReadValue[string](bReader, binary.BigEndian)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -219,12 +222,12 @@ func DecodeProfileContent(data []byte) (*ProfileContent, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
content.Config, err = rw.ReadVString(reader) content.Config, err = varbin.ReadValue[string](bReader, binary.BigEndian)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if content.Type != ProfileTypeLocal { if content.Type != ProfileTypeLocal {
content.RemotePath, err = rw.ReadVString(reader) content.RemotePath, err = varbin.ReadValue[string](bReader, binary.BigEndian)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -149,33 +149,6 @@ func (w *platformInterfaceWrapper) OpenTun(options *tun.Options, platformOptions
return tun.New(*options) return tun.New(*options)
} }
func (w *platformInterfaceWrapper) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*process.Info, error) {
var uid int32
if w.useProcFS {
uid = procfs.ResolveSocketByProcSearch(network, source, destination)
if uid == -1 {
return nil, E.New("procfs: not found")
}
} else {
var ipProtocol int32
switch N.NetworkName(network) {
case N.NetworkTCP:
ipProtocol = syscall.IPPROTO_TCP
case N.NetworkUDP:
ipProtocol = syscall.IPPROTO_UDP
default:
return nil, E.New("unknown network: ", network)
}
var err error
uid, err = w.iif.FindConnectionOwner(ipProtocol, source.Addr().String(), int32(source.Port()), destination.Addr().String(), int32(destination.Port()))
if err != nil {
return nil, err
}
}
packageName, _ := w.iif.PackageNameByUid(uid)
return &process.Info{UserId: uid, PackageName: packageName}, nil
}
func (w *platformInterfaceWrapper) UsePlatformDefaultInterfaceMonitor() bool { func (w *platformInterfaceWrapper) UsePlatformDefaultInterfaceMonitor() bool {
return w.iif.UsePlatformDefaultInterfaceMonitor() return w.iif.UsePlatformDefaultInterfaceMonitor()
} }
@ -229,6 +202,33 @@ func (w *platformInterfaceWrapper) ReadWIFIState() adapter.WIFIState {
return (adapter.WIFIState)(*wifiState) return (adapter.WIFIState)(*wifiState)
} }
func (w *platformInterfaceWrapper) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*process.Info, error) {
var uid int32
if w.useProcFS {
uid = procfs.ResolveSocketByProcSearch(network, source, destination)
if uid == -1 {
return nil, E.New("procfs: not found")
}
} else {
var ipProtocol int32
switch N.NetworkName(network) {
case N.NetworkTCP:
ipProtocol = syscall.IPPROTO_TCP
case N.NetworkUDP:
ipProtocol = syscall.IPPROTO_UDP
default:
return nil, E.New("unknown network: ", network)
}
var err error
uid, err = w.iif.FindConnectionOwner(ipProtocol, source.Addr().String(), int32(source.Port()), destination.Addr().String(), int32(destination.Port()))
if err != nil {
return nil, err
}
}
packageName, _ := w.iif.PackageNameByUid(uid)
return &process.Info{UserId: uid, PackageName: packageName}, nil
}
func (w *platformInterfaceWrapper) DisableColors() bool { func (w *platformInterfaceWrapper) DisableColors() bool {
return runtime.GOOS != "android" return runtime.GOOS != "android"
} }

View File

@ -3,11 +3,14 @@ package libbox
import ( import (
"os" "os"
"os/user" "os/user"
"runtime/debug"
"strconv" "strconv"
"time"
"github.com/sagernet/sing-box/common/humanize" "github.com/sagernet/sing-box/common/humanize"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
_ "github.com/sagernet/sing-box/include" _ "github.com/sagernet/sing-box/include"
"github.com/sagernet/sing-box/log"
) )
var ( var (
@ -19,6 +22,11 @@ var (
sTVOS bool sTVOS bool
) )
func init() {
debug.SetPanicOnFault(true)
debug.SetTraceback("all")
}
func Setup(basePath string, workingPath string, tempPath string, isTVOS bool) { func Setup(basePath string, workingPath string, tempPath string, isTVOS bool) {
sBasePath = basePath sBasePath = basePath
sWorkingPath = workingPath sWorkingPath = workingPath
@ -59,6 +67,10 @@ func FormatMemoryBytes(length int64) string {
return humanize.MemoryBytes(uint64(length)) return humanize.MemoryBytes(uint64(length))
} }
func FormatDuration(duration int64) string {
return log.FormatDuration(time.Duration(duration) * time.Millisecond)
}
func ProxyDisplayType(proxyType string) string { func ProxyDisplayType(proxyType string) string {
return C.ProxyDisplayName(proxyType) return C.ProxyDisplayName(proxyType)
} }

27
go.mod
View File

@ -7,7 +7,6 @@ require (
github.com/caddyserver/certmagic v0.20.0 github.com/caddyserver/certmagic v0.20.0
github.com/cloudflare/circl v1.3.7 github.com/cloudflare/circl v1.3.7
github.com/cretz/bine v0.2.0 github.com/cretz/bine v0.2.0
github.com/fsnotify/fsnotify v1.7.0
github.com/go-chi/chi/v5 v5.0.12 github.com/go-chi/chi/v5 v5.0.12
github.com/go-chi/cors v1.2.1 github.com/go-chi/cors v1.2.1
github.com/go-chi/render v1.0.3 github.com/go-chi/render v1.0.3
@ -17,23 +16,24 @@ require (
github.com/libdns/cloudflare v0.1.1 github.com/libdns/cloudflare v0.1.1
github.com/logrusorgru/aurora v2.0.3+incompatible github.com/logrusorgru/aurora v2.0.3+incompatible
github.com/mholt/acmez v1.2.0 github.com/mholt/acmez v1.2.0
github.com/miekg/dns v1.1.59 github.com/miekg/dns v1.1.61
github.com/ooni/go-libtor v1.1.8 github.com/ooni/go-libtor v1.1.8
github.com/oschwald/maxminddb-golang v1.12.0 github.com/oschwald/maxminddb-golang v1.12.0
github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a
github.com/sagernet/cloudflare-tls v0.0.0-20231208171750-a4483c1b7cd1 github.com/sagernet/cloudflare-tls v0.0.0-20231208171750-a4483c1b7cd1
github.com/sagernet/fswatch v0.1.1
github.com/sagernet/gomobile v0.1.3 github.com/sagernet/gomobile v0.1.3
github.com/sagernet/gvisor v0.0.0-20240428053021-e691de28565f github.com/sagernet/gvisor v0.0.0-20240428053021-e691de28565f
github.com/sagernet/quic-go v0.45.1-beta.2 github.com/sagernet/quic-go v0.45.1-beta.2
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691
github.com/sagernet/sing v0.4.1 github.com/sagernet/sing v0.5.0-alpha.12
github.com/sagernet/sing-dns v0.2.1-0.20240624030536-ca4a5f7afb65 github.com/sagernet/sing-dns v0.3.0-beta.10
github.com/sagernet/sing-mux v0.2.0 github.com/sagernet/sing-mux v0.2.0
github.com/sagernet/sing-quic v0.2.0-beta.12 github.com/sagernet/sing-quic v0.2.0-beta.12
github.com/sagernet/sing-shadowsocks v0.2.7 github.com/sagernet/sing-shadowsocks v0.2.7
github.com/sagernet/sing-shadowsocks2 v0.2.0 github.com/sagernet/sing-shadowsocks2 v0.2.0
github.com/sagernet/sing-shadowtls v0.1.4 github.com/sagernet/sing-shadowtls v0.1.4
github.com/sagernet/sing-tun v0.3.2 github.com/sagernet/sing-tun v0.4.0-beta.13.0.20240703164908-1f043289199d
github.com/sagernet/sing-vmess v0.1.12 github.com/sagernet/sing-vmess v0.1.12
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7
github.com/sagernet/tfo-go v0.0.0-20231209031829-7b5343ac1dc6 github.com/sagernet/tfo-go v0.0.0-20231209031829-7b5343ac1dc6
@ -44,8 +44,8 @@ require (
github.com/stretchr/testify v1.9.0 github.com/stretchr/testify v1.9.0
go.uber.org/zap v1.27.0 go.uber.org/zap v1.27.0
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
golang.org/x/crypto v0.23.0 golang.org/x/crypto v0.24.0
golang.org/x/net v0.25.0 golang.org/x/net v0.26.0
golang.org/x/sys v0.21.0 golang.org/x/sys v0.21.0
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
google.golang.org/grpc v1.63.2 google.golang.org/grpc v1.63.2
@ -59,12 +59,14 @@ require (
github.com/ajg/form v1.5.1 // indirect github.com/ajg/form v1.5.1 // indirect
github.com/andybalholm/brotli v1.0.6 // indirect github.com/andybalholm/brotli v1.0.6 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gaukas/godicttls v0.0.4 // indirect github.com/gaukas/godicttls v0.0.4 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/pool v0.2.1 // indirect
github.com/google/btree v1.1.2 // indirect github.com/google/btree v1.1.2 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a // indirect github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a // indirect
github.com/hashicorp/yamux v0.1.1 // indirect github.com/hashicorp/yamux v0.1.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
@ -72,24 +74,27 @@ require (
github.com/klauspost/compress v1.17.4 // indirect github.com/klauspost/compress v1.17.4 // indirect
github.com/klauspost/cpuid/v2 v2.2.5 // indirect github.com/klauspost/cpuid/v2 v2.2.5 // indirect
github.com/libdns/libdns v0.2.2 // indirect github.com/libdns/libdns v0.2.2 // indirect
github.com/mdlayher/netlink v1.7.2 // indirect
github.com/mdlayher/socket v0.4.1 // indirect
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
github.com/onsi/ginkgo/v2 v2.9.7 // indirect github.com/onsi/ginkgo/v2 v2.9.7 // indirect
github.com/pierrec/lz4/v4 v4.1.14 // indirect github.com/pierrec/lz4/v4 v4.1.14 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/quic-go/qpack v0.4.0 // indirect github.com/quic-go/qpack v0.4.0 // indirect
github.com/quic-go/qtls-go1-20 v0.4.1 // indirect github.com/quic-go/qtls-go1-20 v0.4.1 // indirect
github.com/sagernet/netlink v0.0.0-20240523065131-45e60152f9ba // indirect github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect
github.com/sagernet/nftables v0.3.0-beta.4 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect github.com/vishvananda/netns v0.0.4 // indirect
github.com/zeebo/blake3 v0.2.3 // indirect github.com/zeebo/blake3 v0.2.3 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect
golang.org/x/mod v0.18.0 // indirect golang.org/x/mod v0.18.0 // indirect
golang.org/x/sync v0.7.0 // indirect golang.org/x/sync v0.7.0 // indirect
golang.org/x/text v0.16.0 // indirect golang.org/x/text v0.16.0 // indirect
golang.org/x/time v0.5.0 // indirect golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect golang.org/x/tools v0.22.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect

52
go.sum
View File

@ -40,6 +40,7 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a h1:fEBsGL/sjAuJrgah5XqmmYsTLzJp/TO9Lhy39gkverk= github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a h1:fEBsGL/sjAuJrgah5XqmmYsTLzJp/TO9Lhy39gkverk=
github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
@ -69,10 +70,14 @@ github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s=
github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8=
github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
github.com/mholt/acmez v1.2.0 h1:1hhLxSgY5FvH5HCnGUuwbKY2VQVo8IU7rxXKSnZ7F30= github.com/mholt/acmez v1.2.0 h1:1hhLxSgY5FvH5HCnGUuwbKY2VQVo8IU7rxXKSnZ7F30=
github.com/mholt/acmez v1.2.0/go.mod h1:VT9YwH1xgNX1kmYY89gY8xPJC84BFAisjo8Egigt4kE= github.com/mholt/acmez v1.2.0/go.mod h1:VT9YwH1xgNX1kmYY89gY8xPJC84BFAisjo8Egigt4kE=
github.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs= github.com/miekg/dns v1.1.61 h1:nLxbwF3XxhwVSm8g9Dghm9MHPaUZuqhPiGL+675ZmEs=
github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk= github.com/miekg/dns v1.1.61/go.mod h1:mnAarhS3nWaW+NVP2wTkYVIZyHNJ098SJZUki3eykwQ=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/onsi/ginkgo/v2 v2.9.7 h1:06xGQy5www2oN160RtEZoTvnP2sPhEfePYmCDc2szss= github.com/onsi/ginkgo/v2 v2.9.7 h1:06xGQy5www2oN160RtEZoTvnP2sPhEfePYmCDc2szss=
@ -95,21 +100,25 @@ github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a h1:+NkI2670SQpQWvkk
github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a/go.mod h1:63s7jpZqcDAIpj8oI/1v4Izok+npJOHACFCU6+huCkM= github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a/go.mod h1:63s7jpZqcDAIpj8oI/1v4Izok+npJOHACFCU6+huCkM=
github.com/sagernet/cloudflare-tls v0.0.0-20231208171750-a4483c1b7cd1 h1:YbmpqPQEMdlk9oFSKYWRqVuu9qzNiOayIonKmv1gCXY= github.com/sagernet/cloudflare-tls v0.0.0-20231208171750-a4483c1b7cd1 h1:YbmpqPQEMdlk9oFSKYWRqVuu9qzNiOayIonKmv1gCXY=
github.com/sagernet/cloudflare-tls v0.0.0-20231208171750-a4483c1b7cd1/go.mod h1:J2yAxTFPDjrDPhuAi9aWFz2L3ox9it4qAluBBbN0H5k= github.com/sagernet/cloudflare-tls v0.0.0-20231208171750-a4483c1b7cd1/go.mod h1:J2yAxTFPDjrDPhuAi9aWFz2L3ox9it4qAluBBbN0H5k=
github.com/sagernet/fswatch v0.1.1 h1:YqID+93B7VRfqIH3PArW/XpJv5H4OLEVWDfProGoRQs=
github.com/sagernet/fswatch v0.1.1/go.mod h1:nz85laH0mkQqJfaOrqPpkwtU1znMFNVTpT/5oRsVz/o=
github.com/sagernet/gomobile v0.1.3 h1:ohjIb1Ou2+1558PnZour3od69suSuvkdSVOlO1tC4B8= github.com/sagernet/gomobile v0.1.3 h1:ohjIb1Ou2+1558PnZour3od69suSuvkdSVOlO1tC4B8=
github.com/sagernet/gomobile v0.1.3/go.mod h1:Pqq2+ZVvs10U7xK+UwJgwYWUykewi8H6vlslAO73n9E= github.com/sagernet/gomobile v0.1.3/go.mod h1:Pqq2+ZVvs10U7xK+UwJgwYWUykewi8H6vlslAO73n9E=
github.com/sagernet/gvisor v0.0.0-20240428053021-e691de28565f h1:NkhuupzH5ch7b/Y/6ZHJWrnNLoiNnSJaow6DPb8VW2I= github.com/sagernet/gvisor v0.0.0-20240428053021-e691de28565f h1:NkhuupzH5ch7b/Y/6ZHJWrnNLoiNnSJaow6DPb8VW2I=
github.com/sagernet/gvisor v0.0.0-20240428053021-e691de28565f/go.mod h1:KXmw+ouSJNOsuRpg4wgwwCQuunrGz4yoAqQjsLjc6N0= github.com/sagernet/gvisor v0.0.0-20240428053021-e691de28565f/go.mod h1:KXmw+ouSJNOsuRpg4wgwwCQuunrGz4yoAqQjsLjc6N0=
github.com/sagernet/netlink v0.0.0-20240523065131-45e60152f9ba h1:EY5AS7CCtfmARNv2zXUOrsEMPFDGYxaw65JzA2p51Vk= github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis=
github.com/sagernet/netlink v0.0.0-20240523065131-45e60152f9ba/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I=
github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8=
github.com/sagernet/quic-go v0.45.1-beta.2 h1:zkEeCbhdFFkrxKcuIRBtXNKci/1t2J/39QSG/sPvlmc= github.com/sagernet/quic-go v0.45.1-beta.2 h1:zkEeCbhdFFkrxKcuIRBtXNKci/1t2J/39QSG/sPvlmc=
github.com/sagernet/quic-go v0.45.1-beta.2/go.mod h1:+N3FqM9DAzOWfe64uxXuBejVJwX7DeW7BslzLO6N/xI= github.com/sagernet/quic-go v0.45.1-beta.2/go.mod h1:+N3FqM9DAzOWfe64uxXuBejVJwX7DeW7BslzLO6N/xI=
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byLGkEnIYp6grlXfo1QYUfiYFGjewIdc= github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byLGkEnIYp6grlXfo1QYUfiYFGjewIdc=
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU= github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU=
github.com/sagernet/sing v0.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo= github.com/sagernet/sing v0.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo=
github.com/sagernet/sing v0.4.1 h1:zVlpE+7k7AFoC2pv6ReqLf0PIHjihL/jsBl5k05PQFk= github.com/sagernet/sing v0.5.0-alpha.12 h1:pjffG3SUpuF9PLDCqPO2fOAUozXItIBmnMVTKQ/QMhM=
github.com/sagernet/sing v0.4.1/go.mod h1:ieZHA/+Y9YZfXs2I3WtuwgyCZ6GPsIR7HdKb1SdEnls= github.com/sagernet/sing v0.5.0-alpha.12/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing-dns v0.2.1-0.20240624030536-ca4a5f7afb65 h1:lcCe7E1csuyUA3RCvpFcIYOy6FIifDthKaCrUjLG4xA= github.com/sagernet/sing-dns v0.3.0-beta.10 h1:Js61EjQXVpcu2VDegWEQTH1isCcVwJju8WEHYgG4tQ0=
github.com/sagernet/sing-dns v0.2.1-0.20240624030536-ca4a5f7afb65/go.mod h1:dArgyPZmK8+zDBVRMjV3r12zHgnTara0ahrWwSe/eQE= github.com/sagernet/sing-dns v0.3.0-beta.10/go.mod h1:nXE6EYMXahB5DV3AcXYbFfuorqF7tbQ86kxweSxRKM4=
github.com/sagernet/sing-mux v0.2.0 h1:4C+vd8HztJCWNYfufvgL49xaOoOHXty2+EAjnzN3IYo= github.com/sagernet/sing-mux v0.2.0 h1:4C+vd8HztJCWNYfufvgL49xaOoOHXty2+EAjnzN3IYo=
github.com/sagernet/sing-mux v0.2.0/go.mod h1:khzr9AOPocLa+g53dBplwNDz4gdsyx/YM3swtAhlkHQ= github.com/sagernet/sing-mux v0.2.0/go.mod h1:khzr9AOPocLa+g53dBplwNDz4gdsyx/YM3swtAhlkHQ=
github.com/sagernet/sing-quic v0.2.0-beta.12 h1:BhvA5mmrDFEyDUQB5eeu+9UhF+ieyuNJ5Rsb0dAG3QY= github.com/sagernet/sing-quic v0.2.0-beta.12 h1:BhvA5mmrDFEyDUQB5eeu+9UhF+ieyuNJ5Rsb0dAG3QY=
@ -120,8 +129,8 @@ github.com/sagernet/sing-shadowsocks2 v0.2.0 h1:wpZNs6wKnR7mh1wV9OHwOyUr21VkS3wK
github.com/sagernet/sing-shadowsocks2 v0.2.0/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ= github.com/sagernet/sing-shadowsocks2 v0.2.0/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ=
github.com/sagernet/sing-shadowtls v0.1.4 h1:aTgBSJEgnumzFenPvc+kbD9/W0PywzWevnVpEx6Tw3k= github.com/sagernet/sing-shadowtls v0.1.4 h1:aTgBSJEgnumzFenPvc+kbD9/W0PywzWevnVpEx6Tw3k=
github.com/sagernet/sing-shadowtls v0.1.4/go.mod h1:F8NBgsY5YN2beQavdgdm1DPlhaKQlaL6lpDdcBglGK4= github.com/sagernet/sing-shadowtls v0.1.4/go.mod h1:F8NBgsY5YN2beQavdgdm1DPlhaKQlaL6lpDdcBglGK4=
github.com/sagernet/sing-tun v0.3.2 h1:z0bLUT/YXH9RrJS9DsIpB0Bb9afl2hVJOmHd0zA3HJY= github.com/sagernet/sing-tun v0.4.0-beta.13.0.20240703164908-1f043289199d h1:2nBM9W9fOCM45hjlu1Fh9qyzBCgKEkq+SOuRCbCCs7c=
github.com/sagernet/sing-tun v0.3.2/go.mod h1:DxLIyhjWU/HwGYoX0vNGg2c5QgTQIakphU1MuERR5tQ= github.com/sagernet/sing-tun v0.4.0-beta.13.0.20240703164908-1f043289199d/go.mod h1:81JwnnYw8X9W9XvmZetSTTiPgIE3SbAbnc+EHKwPJ5U=
github.com/sagernet/sing-vmess v0.1.12 h1:2gFD8JJb+eTFMoa8FIVMnknEi+vCSfaiTXTfEYAYAPg= github.com/sagernet/sing-vmess v0.1.12 h1:2gFD8JJb+eTFMoa8FIVMnknEi+vCSfaiTXTfEYAYAPg=
github.com/sagernet/sing-vmess v0.1.12/go.mod h1:luTSsfyBGAc9VhtCqwjR+dt1QgqBhuYBCONB/POhF8I= github.com/sagernet/sing-vmess v0.1.12/go.mod h1:luTSsfyBGAc9VhtCqwjR+dt1QgqBhuYBCONB/POhF8I=
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ= github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ=
@ -146,8 +155,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 h1:tHNk7XK9GkmKUR6Gh8gVBKXc2MVSZ4G/NnWLtzw4gNA= github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 h1:tHNk7XK9GkmKUR6Gh8gVBKXc2MVSZ4G/NnWLtzw4gNA=
github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264= github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264=
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 h1:gga7acRE695APm9hlsSMoOoE65U4/TcqNj90mc69Rlg= github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY= github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg= github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg=
@ -163,20 +172,19 @@ go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBs
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY= golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY=
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -187,7 +195,7 @@ golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
@ -195,8 +203,8 @@ golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 h1:CawjfCvYQH2OU3/TnxLx97WDSUDRABfT18pCOYwc2GE= golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 h1:CawjfCvYQH2OU3/TnxLx97WDSUDRABfT18pCOYwc2GE=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6/go.mod h1:3rxYc4HtVcSG9gVaTs2GEBdehh+sYPOwKtyUWEOTb80= golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6/go.mod h1:3rxYc4HtVcSG9gVaTs2GEBdehh+sYPOwKtyUWEOTb80=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de h1:cZGRis4/ot9uVm639a+rHCUaG0JJHEsdyzSQTMX+suY= google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de h1:cZGRis4/ot9uVm639a+rHCUaG0JJHEsdyzSQTMX+suY=

View File

@ -11,43 +11,43 @@ import (
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
) )
func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, options option.Inbound, platformInterface platform.Interface) (adapter.Inbound, error) { func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.Inbound, platformInterface platform.Interface) (adapter.Inbound, error) {
if options.Type == "" { if options.Type == "" {
return nil, E.New("missing inbound type") return nil, E.New("missing inbound type")
} }
switch options.Type { switch options.Type {
case C.TypeTun: case C.TypeTun:
return NewTun(ctx, router, logger, options.Tag, options.TunOptions, platformInterface) return NewTun(ctx, router, logger, tag, options.TunOptions, platformInterface)
case C.TypeRedirect: case C.TypeRedirect:
return NewRedirect(ctx, router, logger, options.Tag, options.RedirectOptions), nil return NewRedirect(ctx, router, logger, tag, options.RedirectOptions), nil
case C.TypeTProxy: case C.TypeTProxy:
return NewTProxy(ctx, router, logger, options.Tag, options.TProxyOptions), nil return NewTProxy(ctx, router, logger, tag, options.TProxyOptions), nil
case C.TypeDirect: case C.TypeDirect:
return NewDirect(ctx, router, logger, options.Tag, options.DirectOptions), nil return NewDirect(ctx, router, logger, tag, options.DirectOptions), nil
case C.TypeSOCKS: case C.TypeSOCKS:
return NewSocks(ctx, router, logger, options.Tag, options.SocksOptions), nil return NewSocks(ctx, router, logger, tag, options.SocksOptions), nil
case C.TypeHTTP: case C.TypeHTTP:
return NewHTTP(ctx, router, logger, options.Tag, options.HTTPOptions) return NewHTTP(ctx, router, logger, tag, options.HTTPOptions)
case C.TypeMixed: case C.TypeMixed:
return NewMixed(ctx, router, logger, options.Tag, options.MixedOptions), nil return NewMixed(ctx, router, logger, tag, options.MixedOptions), nil
case C.TypeShadowsocks: case C.TypeShadowsocks:
return NewShadowsocks(ctx, router, logger, options.Tag, options.ShadowsocksOptions) return NewShadowsocks(ctx, router, logger, tag, options.ShadowsocksOptions)
case C.TypeVMess: case C.TypeVMess:
return NewVMess(ctx, router, logger, options.Tag, options.VMessOptions) return NewVMess(ctx, router, logger, tag, options.VMessOptions)
case C.TypeTrojan: case C.TypeTrojan:
return NewTrojan(ctx, router, logger, options.Tag, options.TrojanOptions) return NewTrojan(ctx, router, logger, tag, options.TrojanOptions)
case C.TypeNaive: case C.TypeNaive:
return NewNaive(ctx, router, logger, options.Tag, options.NaiveOptions) return NewNaive(ctx, router, logger, tag, options.NaiveOptions)
case C.TypeHysteria: case C.TypeHysteria:
return NewHysteria(ctx, router, logger, options.Tag, options.HysteriaOptions) return NewHysteria(ctx, router, logger, tag, options.HysteriaOptions)
case C.TypeShadowTLS: case C.TypeShadowTLS:
return NewShadowTLS(ctx, router, logger, options.Tag, options.ShadowTLSOptions) return NewShadowTLS(ctx, router, logger, tag, options.ShadowTLSOptions)
case C.TypeVLESS: case C.TypeVLESS:
return NewVLESS(ctx, router, logger, options.Tag, options.VLESSOptions) return NewVLESS(ctx, router, logger, tag, options.VLESSOptions)
case C.TypeTUIC: case C.TypeTUIC:
return NewTUIC(ctx, router, logger, options.Tag, options.TUICOptions) return NewTUIC(ctx, router, logger, tag, options.TUICOptions)
case C.TypeHysteria2: case C.TypeHysteria2:
return NewHysteria2(ctx, router, logger, options.Tag, options.Hysteria2Options) return NewHysteria2(ctx, router, logger, tag, options.Hysteria2Options)
default: default:
return nil, E.New("unknown inbound type: ", options.Type) return nil, E.New("unknown inbound type: ", options.Type)
} }

View File

@ -12,10 +12,7 @@ import (
"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/common/auth" "github.com/sagernet/sing/common/auth"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/bufio"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/rw"
"github.com/sagernet/sing/protocol/http" "github.com/sagernet/sing/protocol/http"
"github.com/sagernet/sing/protocol/socks" "github.com/sagernet/sing/protocol/socks"
"github.com/sagernet/sing/protocol/socks/socks4" "github.com/sagernet/sing/protocol/socks/socks4"
@ -51,16 +48,17 @@ func NewMixed(ctx context.Context, router adapter.Router, logger log.ContextLogg
} }
func (h *Mixed) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { func (h *Mixed) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
headerType, err := rw.ReadByte(conn) reader := std_bufio.NewReader(conn)
headerBytes, err := reader.Peek(1)
if err != nil { if err != nil {
return err return err
} }
switch headerType { switch headerBytes[0] {
case socks4.Version, socks5.Version: case socks4.Version, socks5.Version:
return socks.HandleConnection0(ctx, conn, headerType, h.authenticator, h.upstreamUserHandler(metadata), adapter.UpstreamMetadata(metadata)) return socks.HandleConnection0(ctx, conn, reader, h.authenticator, h.upstreamUserHandler(metadata), adapter.UpstreamMetadata(metadata))
default:
return http.HandleConnection(ctx, conn, reader, h.authenticator, h.upstreamUserHandler(metadata), adapter.UpstreamMetadata(metadata))
} }
reader := std_bufio.NewReader(bufio.NewCachedReader(conn, buf.As([]byte{headerType})))
return http.HandleConnection(ctx, conn, reader, h.authenticator, h.upstreamUserHandler(metadata), adapter.UpstreamMetadata(metadata))
} }
func (h *Mixed) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { func (h *Mixed) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {

View File

@ -3,6 +3,9 @@ package inbound
import ( import (
"context" "context"
"net" "net"
"net/netip"
"os"
"runtime"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@ -19,27 +22,91 @@ import (
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"
"github.com/sagernet/sing/common/ranges" "github.com/sagernet/sing/common/ranges"
"github.com/sagernet/sing/common/x/list"
"go4.org/netipx"
) )
var _ adapter.Inbound = (*Tun)(nil) var _ adapter.Inbound = (*Tun)(nil)
type Tun struct { type Tun struct {
tag string tag string
ctx context.Context ctx context.Context
router adapter.Router router adapter.Router
logger log.ContextLogger logger log.ContextLogger
inboundOptions option.InboundOptions inboundOptions option.InboundOptions
tunOptions tun.Options tunOptions tun.Options
endpointIndependentNat bool endpointIndependentNat bool
udpTimeout int64 udpTimeout int64
stack string stack string
tunIf tun.Tun tunIf tun.Tun
tunStack tun.Stack tunStack tun.Stack
platformInterface platform.Interface platformInterface platform.Interface
platformOptions option.TunPlatformOptions platformOptions option.TunPlatformOptions
autoRedirect tun.AutoRedirect
routeRuleSet []adapter.RuleSet
routeRuleSetCallback []*list.Element[adapter.RuleSetUpdateCallback]
routeExcludeRuleSet []adapter.RuleSet
routeExcludeRuleSetCallback []*list.Element[adapter.RuleSetUpdateCallback]
routeAddressSet []*netipx.IPSet
routeExcludeAddressSet []*netipx.IPSet
} }
func NewTun(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TunInboundOptions, platformInterface platform.Interface) (*Tun, error) { func NewTun(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TunInboundOptions, platformInterface platform.Interface) (*Tun, error) {
address := options.Address
//nolint:staticcheck
//goland:noinspection GoDeprecation
if len(options.Inet4Address) > 0 {
address = append(address, options.Inet4Address...)
}
//nolint:staticcheck
//goland:noinspection GoDeprecation
if len(options.Inet6Address) > 0 {
address = append(address, options.Inet6Address...)
}
inet4Address := common.Filter(address, func(it netip.Prefix) bool {
return it.Addr().Is4()
})
inet6Address := common.Filter(address, func(it netip.Prefix) bool {
return it.Addr().Is6()
})
routeAddress := options.RouteAddress
//nolint:staticcheck
//goland:noinspection GoDeprecation
if len(options.Inet4RouteAddress) > 0 {
routeAddress = append(routeAddress, options.Inet4RouteAddress...)
}
//nolint:staticcheck
//goland:noinspection GoDeprecation
if len(options.Inet6RouteAddress) > 0 {
routeAddress = append(routeAddress, options.Inet6RouteAddress...)
}
inet4RouteAddress := common.Filter(routeAddress, func(it netip.Prefix) bool {
return it.Addr().Is4()
})
inet6RouteAddress := common.Filter(routeAddress, func(it netip.Prefix) bool {
return it.Addr().Is6()
})
routeExcludeAddress := options.RouteExcludeAddress
//nolint:staticcheck
//goland:noinspection GoDeprecation
if len(options.Inet4RouteExcludeAddress) > 0 {
routeExcludeAddress = append(routeExcludeAddress, options.Inet4RouteExcludeAddress...)
}
//nolint:staticcheck
//goland:noinspection GoDeprecation
if len(options.Inet6RouteExcludeAddress) > 0 {
routeExcludeAddress = append(routeExcludeAddress, options.Inet6RouteExcludeAddress...)
}
inet4RouteExcludeAddress := common.Filter(routeExcludeAddress, func(it netip.Prefix) bool {
return it.Addr().Is4()
})
inet6RouteExcludeAddress := common.Filter(routeExcludeAddress, func(it netip.Prefix) bool {
return it.Addr().Is6()
})
tunMTU := options.MTU tunMTU := options.MTU
if tunMTU == 0 { if tunMTU == 0 {
tunMTU = 9000 tunMTU = 9000
@ -50,9 +117,9 @@ func NewTun(ctx context.Context, router adapter.Router, logger log.ContextLogger
} else { } else {
udpTimeout = C.UDPTimeout udpTimeout = C.UDPTimeout
} }
var err error
includeUID := uidToRange(options.IncludeUID) includeUID := uidToRange(options.IncludeUID)
if len(options.IncludeUIDRange) > 0 { if len(options.IncludeUIDRange) > 0 {
var err error
includeUID, err = parseRange(includeUID, options.IncludeUIDRange) includeUID, err = parseRange(includeUID, options.IncludeUIDRange)
if err != nil { if err != nil {
return nil, E.Cause(err, "parse include_uid_range") return nil, E.Cause(err, "parse include_uid_range")
@ -60,13 +127,30 @@ func NewTun(ctx context.Context, router adapter.Router, logger log.ContextLogger
} }
excludeUID := uidToRange(options.ExcludeUID) excludeUID := uidToRange(options.ExcludeUID)
if len(options.ExcludeUIDRange) > 0 { if len(options.ExcludeUIDRange) > 0 {
var err error
excludeUID, err = parseRange(excludeUID, options.ExcludeUIDRange) excludeUID, err = parseRange(excludeUID, options.ExcludeUIDRange)
if err != nil { if err != nil {
return nil, E.Cause(err, "parse exclude_uid_range") return nil, E.Cause(err, "parse exclude_uid_range")
} }
} }
return &Tun{
tableIndex := options.IPRoute2TableIndex
if tableIndex == 0 {
tableIndex = tun.DefaultIPRoute2TableIndex
}
ruleIndex := options.IPRoute2RuleIndex
if ruleIndex == 0 {
ruleIndex = tun.DefaultIPRoute2RuleIndex
}
inputMark := uint32(options.AutoRedirectInputMark)
if inputMark == 0 {
inputMark = tun.DefaultAutoRedirectInputMark
}
outputMark := uint32(options.AutoRedirectOutputMark)
if outputMark == 0 {
outputMark = tun.DefaultAutoRedirectOutputMark
}
inbound := &Tun{
tag: tag, tag: tag,
ctx: ctx, ctx: ctx,
router: router, router: router,
@ -76,30 +160,83 @@ func NewTun(ctx context.Context, router adapter.Router, logger log.ContextLogger
Name: options.InterfaceName, Name: options.InterfaceName,
MTU: tunMTU, MTU: tunMTU,
GSO: options.GSO, GSO: options.GSO,
Inet4Address: options.Inet4Address, Inet4Address: inet4Address,
Inet6Address: options.Inet6Address, Inet6Address: inet6Address,
AutoRoute: options.AutoRoute, AutoRoute: options.AutoRoute,
IPRoute2TableIndex: tableIndex,
IPRoute2RuleIndex: ruleIndex,
AutoRedirectInputMark: inputMark,
AutoRedirectOutputMark: outputMark,
StrictRoute: options.StrictRoute, StrictRoute: options.StrictRoute,
IncludeInterface: options.IncludeInterface, IncludeInterface: options.IncludeInterface,
ExcludeInterface: options.ExcludeInterface, ExcludeInterface: options.ExcludeInterface,
Inet4RouteAddress: options.Inet4RouteAddress, Inet4RouteAddress: inet4RouteAddress,
Inet6RouteAddress: options.Inet6RouteAddress, Inet6RouteAddress: inet6RouteAddress,
Inet4RouteExcludeAddress: options.Inet4RouteExcludeAddress, Inet4RouteExcludeAddress: inet4RouteExcludeAddress,
Inet6RouteExcludeAddress: options.Inet6RouteExcludeAddress, Inet6RouteExcludeAddress: inet6RouteExcludeAddress,
IncludeUID: includeUID, IncludeUID: includeUID,
ExcludeUID: excludeUID, ExcludeUID: excludeUID,
IncludeAndroidUser: options.IncludeAndroidUser, IncludeAndroidUser: options.IncludeAndroidUser,
IncludePackage: options.IncludePackage, IncludePackage: options.IncludePackage,
ExcludePackage: options.ExcludePackage, ExcludePackage: options.ExcludePackage,
InterfaceMonitor: router.InterfaceMonitor(), InterfaceMonitor: router.InterfaceMonitor(),
TableIndex: 2022,
}, },
endpointIndependentNat: options.EndpointIndependentNat, endpointIndependentNat: options.EndpointIndependentNat,
udpTimeout: int64(udpTimeout.Seconds()), udpTimeout: int64(udpTimeout.Seconds()),
stack: options.Stack, stack: options.Stack,
platformInterface: platformInterface, platformInterface: platformInterface,
platformOptions: common.PtrValueOrDefault(options.Platform), platformOptions: common.PtrValueOrDefault(options.Platform),
}, nil }
if options.AutoRedirect {
if !options.AutoRoute {
return nil, E.New("`auto_route` is required by `auto_redirect`")
}
disableNFTables, dErr := strconv.ParseBool(os.Getenv("DISABLE_NFTABLES"))
inbound.autoRedirect, err = tun.NewAutoRedirect(tun.AutoRedirectOptions{
TunOptions: &inbound.tunOptions,
Context: ctx,
Handler: inbound,
Logger: logger,
NetworkMonitor: router.NetworkMonitor(),
InterfaceFinder: router.InterfaceFinder(),
TableName: "sing-box",
DisableNFTables: dErr == nil && disableNFTables,
RouteAddressSet: &inbound.routeAddressSet,
RouteExcludeAddressSet: &inbound.routeExcludeAddressSet,
})
if err != nil {
return nil, E.Cause(err, "initialize auto-redirect")
}
if runtime.GOOS != "android" {
var markMode bool
for _, routeAddressSet := range options.RouteAddressSet {
ruleSet, loaded := router.RuleSet(routeAddressSet)
if !loaded {
return nil, E.New("parse route_address_set: rule-set not found: ", routeAddressSet)
}
ruleSet.IncRef()
inbound.routeRuleSet = append(inbound.routeRuleSet, ruleSet)
markMode = true
}
for _, routeExcludeAddressSet := range options.RouteExcludeAddressSet {
ruleSet, loaded := router.RuleSet(routeExcludeAddressSet)
if !loaded {
return nil, E.New("parse route_exclude_address_set: rule-set not found: ", routeExcludeAddressSet)
}
ruleSet.IncRef()
inbound.routeExcludeRuleSet = append(inbound.routeExcludeRuleSet, ruleSet)
markMode = true
}
if markMode {
inbound.tunOptions.AutoRedirectMarkMode = true
err = router.RegisterAutoRedirectOutputMark(inbound.tunOptions.AutoRedirectOutputMark)
if err != nil {
return nil, err
}
}
}
}
return inbound, nil
} }
func uidToRange(uidList option.Listable[uint32]) []ranges.Range[uint32] { func uidToRange(uidList option.Listable[uint32]) []ranges.Range[uint32] {
@ -121,11 +258,11 @@ func parseRange(uidRanges []ranges.Range[uint32], rangeList []string) ([]ranges.
} }
var start, end uint64 var start, end uint64
var err error var err error
start, err = strconv.ParseUint(uidRange[:subIndex], 10, 32) start, err = strconv.ParseUint(uidRange[:subIndex], 0, 32)
if err != nil { if err != nil {
return nil, E.Cause(err, "parse range start") return nil, E.Cause(err, "parse range start")
} }
end, err = strconv.ParseUint(uidRange[subIndex+1:], 10, 32) end, err = strconv.ParseUint(uidRange[subIndex+1:], 0, 32)
if err != nil { if err != nil {
return nil, E.Cause(err, "parse range end") return nil, E.Cause(err, "parse range end")
} }
@ -200,10 +337,58 @@ func (t *Tun) Start() error {
return nil return nil
} }
func (t *Tun) PostStart() error {
monitor := taskmonitor.New(t.logger, C.StartTimeout)
if t.autoRedirect != nil {
t.routeAddressSet = common.FlatMap(t.routeRuleSet, adapter.RuleSet.ExtractIPSet)
for _, routeRuleSet := range t.routeRuleSet {
ipSets := routeRuleSet.ExtractIPSet()
if len(ipSets) == 0 {
t.logger.Warn("route_address_set: no destination IP CIDR rules found in rule-set: ", routeRuleSet.Name())
}
t.routeAddressSet = append(t.routeAddressSet, ipSets...)
}
t.routeExcludeAddressSet = common.FlatMap(t.routeExcludeRuleSet, adapter.RuleSet.ExtractIPSet)
for _, routeExcludeRuleSet := range t.routeExcludeRuleSet {
ipSets := routeExcludeRuleSet.ExtractIPSet()
if len(ipSets) == 0 {
t.logger.Warn("route_address_set: no destination IP CIDR rules found in rule-set: ", routeExcludeRuleSet.Name())
}
t.routeExcludeAddressSet = append(t.routeExcludeAddressSet, ipSets...)
}
monitor.Start("initialize auto-redirect")
err := t.autoRedirect.Start()
monitor.Finish()
if err != nil {
return E.Cause(err, "auto-redirect")
}
for _, routeRuleSet := range t.routeRuleSet {
t.routeRuleSetCallback = append(t.routeRuleSetCallback, routeRuleSet.RegisterCallback(t.updateRouteAddressSet))
routeRuleSet.DecRef()
}
for _, routeExcludeRuleSet := range t.routeExcludeRuleSet {
t.routeExcludeRuleSetCallback = append(t.routeExcludeRuleSetCallback, routeExcludeRuleSet.RegisterCallback(t.updateRouteAddressSet))
routeExcludeRuleSet.DecRef()
}
t.routeAddressSet = nil
t.routeExcludeAddressSet = nil
}
return nil
}
func (t *Tun) updateRouteAddressSet(it adapter.RuleSet) {
t.routeAddressSet = common.FlatMap(t.routeRuleSet, adapter.RuleSet.ExtractIPSet)
t.routeExcludeAddressSet = common.FlatMap(t.routeExcludeRuleSet, adapter.RuleSet.ExtractIPSet)
t.autoRedirect.UpdateRouteAddressSet()
t.routeAddressSet = nil
t.routeExcludeAddressSet = nil
}
func (t *Tun) Close() error { func (t *Tun) Close() error {
return common.Close( return common.Close(
t.tunStack, t.tunStack,
t.tunIf, t.tunIf,
t.autoRedirect,
) )
} }
@ -215,7 +400,11 @@ func (t *Tun) NewConnection(ctx context.Context, conn net.Conn, upstreamMetadata
metadata.Source = upstreamMetadata.Source metadata.Source = upstreamMetadata.Source
metadata.Destination = upstreamMetadata.Destination metadata.Destination = upstreamMetadata.Destination
metadata.InboundOptions = t.inboundOptions metadata.InboundOptions = t.inboundOptions
t.logger.InfoContext(ctx, "inbound connection from ", metadata.Source) if upstreamMetadata.Protocol != "" {
t.logger.InfoContext(ctx, "inbound ", upstreamMetadata.Protocol, " connection from ", metadata.Source)
} else {
t.logger.InfoContext(ctx, "inbound connection from ", metadata.Source)
}
t.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) t.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination)
err := t.router.RouteConnection(ctx, conn, metadata) err := t.router.RouteConnection(ctx, conn, metadata)
if err != nil { if err != nil {

View File

@ -83,12 +83,11 @@ func NewVLESS(ctx context.Context, router adapter.Router, logger log.ContextLogg
} }
func (h *VLESS) Start() error { func (h *VLESS) Start() error {
err := common.Start( if h.tlsConfig != nil {
h.service, err := h.tlsConfig.Start()
h.tlsConfig, if err != nil {
) return err
if err != nil { }
return err
} }
if h.transport == nil { if h.transport == nil {
return h.myInboundAdapter.Start() return h.myInboundAdapter.Start()

View File

@ -93,13 +93,16 @@ func NewVMess(ctx context.Context, router adapter.Router, logger log.ContextLogg
} }
func (h *VMess) Start() error { func (h *VMess) Start() error {
err := common.Start( err := h.service.Start()
h.service,
h.tlsConfig,
)
if err != nil { if err != nil {
return err return err
} }
if h.tlsConfig != nil {
err = h.tlsConfig.Start()
if err != nil {
return err
}
}
if h.transport == nil { if h.transport == nil {
return h.myInboundAdapter.Start() return h.myInboundAdapter.Start()
} }

View File

@ -43,7 +43,7 @@ func (f Formatter) Format(ctx context.Context, level Level, tag string, message
id, hasId = IDFromContext(ctx) id, hasId = IDFromContext(ctx)
} }
if hasId { if hasId {
activeDuration := formatDuration(time.Since(id.CreatedAt)) activeDuration := FormatDuration(time.Since(id.CreatedAt))
if !f.DisableColors { if !f.DisableColors {
var color aurora.Color var color aurora.Color
color = aurora.Color(uint8(id.ID)) color = aurora.Color(uint8(id.ID))
@ -113,7 +113,7 @@ func (f Formatter) FormatWithSimple(ctx context.Context, level Level, tag string
id, hasId = IDFromContext(ctx) id, hasId = IDFromContext(ctx)
} }
if hasId { if hasId {
activeDuration := formatDuration(time.Since(id.CreatedAt)) activeDuration := FormatDuration(time.Since(id.CreatedAt))
if !f.DisableColors { if !f.DisableColors {
var color aurora.Color var color aurora.Color
color = aurora.Color(uint8(id.ID)) color = aurora.Color(uint8(id.ID))
@ -163,7 +163,7 @@ func xd(value int, x int) string {
return message return message
} }
func formatDuration(duration time.Duration) string { func FormatDuration(duration time.Duration) string {
if duration < time.Second { if duration < time.Second {
return F.ToString(duration.Milliseconds(), "ms") return F.ToString(duration.Milliseconds(), "ms")
} else if duration < time.Minute { } else if duration < time.Minute {

View File

@ -113,7 +113,7 @@ type DialerOptions struct {
Inet4BindAddress *ListenAddress `json:"inet4_bind_address,omitempty"` Inet4BindAddress *ListenAddress `json:"inet4_bind_address,omitempty"`
Inet6BindAddress *ListenAddress `json:"inet6_bind_address,omitempty"` Inet6BindAddress *ListenAddress `json:"inet6_bind_address,omitempty"`
ProtectPath string `json:"protect_path,omitempty"` ProtectPath string `json:"protect_path,omitempty"`
RoutingMark int `json:"routing_mark,omitempty"` RoutingMark uint32 `json:"routing_mark,omitempty"`
ReuseAddr bool `json:"reuse_addr,omitempty"` ReuseAddr bool `json:"reuse_addr,omitempty"`
ConnectTimeout Duration `json:"connect_timeout,omitempty"` ConnectTimeout Duration `json:"connect_timeout,omitempty"`
TCPFastOpen bool `json:"tcp_fast_open,omitempty"` TCPFastOpen bool `json:"tcp_fast_open,omitempty"`

View File

@ -10,7 +10,7 @@ type RouteOptions struct {
AutoDetectInterface bool `json:"auto_detect_interface,omitempty"` AutoDetectInterface bool `json:"auto_detect_interface,omitempty"`
OverrideAndroidVPN bool `json:"override_android_vpn,omitempty"` OverrideAndroidVPN bool `json:"override_android_vpn,omitempty"`
DefaultInterface string `json:"default_interface,omitempty"` DefaultInterface string `json:"default_interface,omitempty"`
DefaultMark int `json:"default_mark,omitempty"` DefaultMark uint32 `json:"default_mark,omitempty"`
} }
type GeoIPOptions struct { type GeoIPOptions struct {

View File

@ -64,7 +64,7 @@ func (r Rule) IsValid() bool {
} }
} }
type DefaultRule struct { type _DefaultRule struct {
Inbound Listable[string] `json:"inbound,omitempty"` Inbound Listable[string] `json:"inbound,omitempty"`
IPVersion int `json:"ip_version,omitempty"` IPVersion int `json:"ip_version,omitempty"`
Network Listable[string] `json:"network,omitempty"` Network Listable[string] `json:"network,omitempty"`
@ -94,12 +94,31 @@ type DefaultRule struct {
WIFISSID Listable[string] `json:"wifi_ssid,omitempty"` WIFISSID Listable[string] `json:"wifi_ssid,omitempty"`
WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"` WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"`
RuleSet Listable[string] `json:"rule_set,omitempty"` RuleSet Listable[string] `json:"rule_set,omitempty"`
RuleSetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,omitempty"` RuleSetIPCIDRMatchSource bool `json:"rule_set_ip_cidr_match_source,omitempty"`
Invert bool `json:"invert,omitempty"` Invert bool `json:"invert,omitempty"`
Outbound string `json:"outbound,omitempty"` Outbound string `json:"outbound,omitempty"`
// Deprecated: renamed to rule_set_ip_cidr_match_source
Deprecated_RulesetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,omitempty"`
} }
func (r DefaultRule) IsValid() bool { type DefaultRule _DefaultRule
func (r *DefaultRule) UnmarshalJSON(bytes []byte) error {
err := json.Unmarshal(bytes, (*_DefaultRule)(r))
if err != nil {
return err
}
//nolint:staticcheck
//goland:noinspection GoDeprecation
if r.Deprecated_RulesetIPCIDRMatchSource {
r.Deprecated_RulesetIPCIDRMatchSource = false
r.RuleSetIPCIDRMatchSource = true
}
return nil
}
func (r *DefaultRule) IsValid() bool {
var defaultValue DefaultRule var defaultValue DefaultRule
defaultValue.Invert = r.Invert defaultValue.Invert = r.Invert
defaultValue.Outbound = r.Outbound defaultValue.Outbound = r.Outbound

View File

@ -64,7 +64,7 @@ func (r DNSRule) IsValid() bool {
} }
} }
type DefaultDNSRule struct { type _DefaultDNSRule struct {
Inbound Listable[string] `json:"inbound,omitempty"` Inbound Listable[string] `json:"inbound,omitempty"`
IPVersion int `json:"ip_version,omitempty"` IPVersion int `json:"ip_version,omitempty"`
QueryType Listable[DNSQueryType] `json:"query_type,omitempty"` QueryType Listable[DNSQueryType] `json:"query_type,omitempty"`
@ -96,15 +96,35 @@ type DefaultDNSRule struct {
WIFISSID Listable[string] `json:"wifi_ssid,omitempty"` WIFISSID Listable[string] `json:"wifi_ssid,omitempty"`
WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"` WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"`
RuleSet Listable[string] `json:"rule_set,omitempty"` RuleSet Listable[string] `json:"rule_set,omitempty"`
RuleSetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,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"` Invert bool `json:"invert,omitempty"`
Server string `json:"server,omitempty"` Server string `json:"server,omitempty"`
DisableCache bool `json:"disable_cache,omitempty"` DisableCache bool `json:"disable_cache,omitempty"`
RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"`
ClientSubnet *AddrPrefix `json:"client_subnet,omitempty"` ClientSubnet *AddrPrefix `json:"client_subnet,omitempty"`
// Deprecated: renamed to rule_set_ip_cidr_match_source
Deprecated_RulesetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,omitempty"`
} }
func (r DefaultDNSRule) IsValid() bool { type DefaultDNSRule _DefaultDNSRule
func (r *DefaultDNSRule) UnmarshalJSON(bytes []byte) error {
err := json.Unmarshal(bytes, (*_DefaultDNSRule)(r))
if err != nil {
return err
}
//nolint:staticcheck
//goland:noinspection GoDeprecation
if r.Deprecated_RulesetIPCIDRMatchSource {
r.Deprecated_RulesetIPCIDRMatchSource = false
r.RuleSetIPCIDRMatchSource = true
}
return nil
}
func (r *DefaultDNSRule) IsValid() bool {
var defaultValue DefaultDNSRule var defaultValue DefaultDNSRule
defaultValue.Invert = r.Invert defaultValue.Invert = r.Invert
defaultValue.Server = r.Server defaultValue.Server = r.Server

View File

@ -17,6 +17,7 @@ type _RuleSet struct {
Type string `json:"type"` Type string `json:"type"`
Tag string `json:"tag"` Tag string `json:"tag"`
Format string `json:"format"` Format string `json:"format"`
InlineOptions PlainRuleSet `json:"-"`
LocalOptions LocalRuleSet `json:"-"` LocalOptions LocalRuleSet `json:"-"`
RemoteOptions RemoteRuleSet `json:"-"` RemoteOptions RemoteRuleSet `json:"-"`
} }
@ -26,12 +27,15 @@ type RuleSet _RuleSet
func (r RuleSet) MarshalJSON() ([]byte, error) { func (r RuleSet) MarshalJSON() ([]byte, error) {
var v any var v any
switch r.Type { switch r.Type {
case "", C.RuleSetTypeInline:
r.Type = ""
v = r.InlineOptions
case C.RuleSetTypeLocal: case C.RuleSetTypeLocal:
v = r.LocalOptions v = r.LocalOptions
case C.RuleSetTypeRemote: case C.RuleSetTypeRemote:
v = r.RemoteOptions v = r.RemoteOptions
default: default:
return nil, E.New("unknown rule set type: " + r.Type) return nil, E.New("unknown rule-set type: " + r.Type)
} }
return MarshallObjects((_RuleSet)(r), v) return MarshallObjects((_RuleSet)(r), v)
} }
@ -44,23 +48,28 @@ func (r *RuleSet) UnmarshalJSON(bytes []byte) error {
if r.Tag == "" { if r.Tag == "" {
return E.New("missing tag") return E.New("missing tag")
} }
switch r.Format { if r.Type != C.RuleSetTypeInline {
case "": switch r.Format {
return E.New("missing format") case "":
case C.RuleSetFormatSource, C.RuleSetFormatBinary: return E.New("missing format")
default: case C.RuleSetFormatSource, C.RuleSetFormatBinary:
return E.New("unknown rule set format: " + r.Format) default:
return E.New("unknown rule-set format: " + r.Format)
}
} else {
r.Format = ""
} }
var v any var v any
switch r.Type { switch r.Type {
case "", C.RuleSetTypeInline:
r.Type = C.RuleSetTypeInline
v = &r.InlineOptions
case C.RuleSetTypeLocal: case C.RuleSetTypeLocal:
v = &r.LocalOptions v = &r.LocalOptions
case C.RuleSetTypeRemote: case C.RuleSetTypeRemote:
v = &r.RemoteOptions v = &r.RemoteOptions
case "":
return E.New("missing type")
default: default:
return E.New("unknown rule set type: " + r.Type) return E.New("unknown rule-set type: " + r.Type)
} }
err = UnmarshallExcluded(bytes, (*_RuleSet)(r), v) err = UnmarshallExcluded(bytes, (*_RuleSet)(r), v)
if err != nil { if err != nil {
@ -188,7 +197,7 @@ func (r PlainRuleSetCompat) MarshalJSON() ([]byte, error) {
case C.RuleSetVersion1: case C.RuleSetVersion1:
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)
} }
return MarshallObjects((_PlainRuleSetCompat)(r), v) return MarshallObjects((_PlainRuleSetCompat)(r), v)
} }
@ -203,9 +212,9 @@ func (r *PlainRuleSetCompat) UnmarshalJSON(bytes []byte) error {
case C.RuleSetVersion1: case C.RuleSetVersion1:
v = &r.Options v = &r.Options
case 0: case 0:
return E.New("missing rule set version") return E.New("missing rule-set version")
default: default:
return E.New("unknown rule set version: ", r.Version) return E.New("unknown rule-set version: ", r.Version)
} }
err = UnmarshallExcluded(bytes, (*_PlainRuleSetCompat)(r), v) err = UnmarshallExcluded(bytes, (*_PlainRuleSetCompat)(r), v)
if err != nil { if err != nil {
@ -214,15 +223,13 @@ func (r *PlainRuleSetCompat) UnmarshalJSON(bytes []byte) error {
return nil return nil
} }
func (r PlainRuleSetCompat) Upgrade() PlainRuleSet { func (r PlainRuleSetCompat) Upgrade() (PlainRuleSet, error) {
var result PlainRuleSet
switch r.Version { switch r.Version {
case C.RuleSetVersion1: case C.RuleSetVersion1:
result = r.Options
default: default:
panic("unknown rule set version: " + F.ToString(r.Version)) return PlainRuleSet{}, E.New("unknown rule-set version: " + F.ToString(r.Version))
} }
return result return r.Options, nil
} }
type PlainRuleSet struct { type PlainRuleSet struct {

View File

@ -1,31 +1,78 @@
package option package option
import "net/netip" import (
"net/netip"
"strconv"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/common/json"
)
type TunInboundOptions struct { type TunInboundOptions struct {
InterfaceName string `json:"interface_name,omitempty"` InterfaceName string `json:"interface_name,omitempty"`
MTU uint32 `json:"mtu,omitempty"` MTU uint32 `json:"mtu,omitempty"`
GSO bool `json:"gso,omitempty"` GSO bool `json:"gso,omitempty"`
Inet4Address Listable[netip.Prefix] `json:"inet4_address,omitempty"` Address Listable[netip.Prefix] `json:"address,omitempty"`
Inet6Address Listable[netip.Prefix] `json:"inet6_address,omitempty"` AutoRoute bool `json:"auto_route,omitempty"`
AutoRoute bool `json:"auto_route,omitempty"` IPRoute2TableIndex int `json:"iproute2_table_index,omitempty"`
StrictRoute bool `json:"strict_route,omitempty"` IPRoute2RuleIndex int `json:"iproute2_rule_index,omitempty"`
Inet4RouteAddress Listable[netip.Prefix] `json:"inet4_route_address,omitempty"` AutoRedirect bool `json:"auto_redirect,omitempty"`
Inet6RouteAddress Listable[netip.Prefix] `json:"inet6_route_address,omitempty"` AutoRedirectInputMark FwMark `json:"auto_redirect_input_mark,omitempty"`
Inet4RouteExcludeAddress Listable[netip.Prefix] `json:"inet4_route_exclude_address,omitempty"` AutoRedirectOutputMark FwMark `json:"auto_redirect_output_mark,omitempty"`
Inet6RouteExcludeAddress Listable[netip.Prefix] `json:"inet6_route_exclude_address,omitempty"` StrictRoute bool `json:"strict_route,omitempty"`
IncludeInterface Listable[string] `json:"include_interface,omitempty"` RouteAddress Listable[netip.Prefix] `json:"route_address,omitempty"`
ExcludeInterface Listable[string] `json:"exclude_interface,omitempty"` RouteAddressSet Listable[string] `json:"route_address_set,omitempty"`
IncludeUID Listable[uint32] `json:"include_uid,omitempty"` RouteExcludeAddress Listable[netip.Prefix] `json:"route_exclude_address,omitempty"`
IncludeUIDRange Listable[string] `json:"include_uid_range,omitempty"` RouteExcludeAddressSet Listable[string] `json:"route_exclude_address_set,omitempty"`
ExcludeUID Listable[uint32] `json:"exclude_uid,omitempty"` IncludeInterface Listable[string] `json:"include_interface,omitempty"`
ExcludeUIDRange Listable[string] `json:"exclude_uid_range,omitempty"` ExcludeInterface Listable[string] `json:"exclude_interface,omitempty"`
IncludeAndroidUser Listable[int] `json:"include_android_user,omitempty"` IncludeUID Listable[uint32] `json:"include_uid,omitempty"`
IncludePackage Listable[string] `json:"include_package,omitempty"` IncludeUIDRange Listable[string] `json:"include_uid_range,omitempty"`
ExcludePackage Listable[string] `json:"exclude_package,omitempty"` ExcludeUID Listable[uint32] `json:"exclude_uid,omitempty"`
EndpointIndependentNat bool `json:"endpoint_independent_nat,omitempty"` ExcludeUIDRange Listable[string] `json:"exclude_uid_range,omitempty"`
UDPTimeout UDPTimeoutCompat `json:"udp_timeout,omitempty"` IncludeAndroidUser Listable[int] `json:"include_android_user,omitempty"`
Stack string `json:"stack,omitempty"` IncludePackage Listable[string] `json:"include_package,omitempty"`
Platform *TunPlatformOptions `json:"platform,omitempty"` ExcludePackage Listable[string] `json:"exclude_package,omitempty"`
EndpointIndependentNat bool `json:"endpoint_independent_nat,omitempty"`
UDPTimeout UDPTimeoutCompat `json:"udp_timeout,omitempty"`
Stack string `json:"stack,omitempty"`
Platform *TunPlatformOptions `json:"platform,omitempty"`
InboundOptions InboundOptions
// Deprecated: merged to Address
Inet4Address Listable[netip.Prefix] `json:"inet4_address,omitempty"`
// Deprecated: merged to Address
Inet6Address Listable[netip.Prefix] `json:"inet6_address,omitempty"`
// Deprecated: merged to RouteAddress
Inet4RouteAddress Listable[netip.Prefix] `json:"inet4_route_address,omitempty"`
// Deprecated: merged to RouteAddress
Inet6RouteAddress Listable[netip.Prefix] `json:"inet6_route_address,omitempty"`
// Deprecated: merged to RouteExcludeAddress
Inet4RouteExcludeAddress Listable[netip.Prefix] `json:"inet4_route_exclude_address,omitempty"`
// Deprecated: merged to RouteExcludeAddress
Inet6RouteExcludeAddress Listable[netip.Prefix] `json:"inet6_route_exclude_address,omitempty"`
}
type FwMark uint32
func (f FwMark) MarshalJSON() ([]byte, error) {
return json.Marshal(F.ToString("0x", strconv.FormatUint(uint64(f), 16)))
}
func (f *FwMark) UnmarshalJSON(bytes []byte) error {
var stringValue string
err := json.Unmarshal(bytes, &stringValue)
if err != nil {
if rawErr := json.Unmarshal(bytes, (*uint32)(f)); rawErr == nil {
return nil
}
return E.Cause(err, "invalid number or string mark")
}
intValue, err := strconv.ParseUint(stringValue, 0, 32)
if err != nil {
return err
}
*f = FwMark(intValue)
return nil
} }

View File

@ -1,7 +1,6 @@
package outbound package outbound
import ( import (
std_bufio "bufio"
"context" "context"
"crypto/rand" "crypto/rand"
"encoding/hex" "encoding/hex"
@ -11,16 +10,10 @@ import (
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/auth" "github.com/sagernet/sing/common/auth"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/bufio"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/rw"
"github.com/sagernet/sing/protocol/http"
"github.com/sagernet/sing/protocol/socks" "github.com/sagernet/sing/protocol/socks"
"github.com/sagernet/sing/protocol/socks/socks4"
"github.com/sagernet/sing/protocol/socks/socks5"
) )
type ProxyListener struct { type ProxyListener struct {
@ -102,16 +95,7 @@ func (l *ProxyListener) acceptLoop() {
} }
func (l *ProxyListener) accept(ctx context.Context, conn *net.TCPConn) error { func (l *ProxyListener) accept(ctx context.Context, conn *net.TCPConn) error {
headerType, err := rw.ReadByte(conn) return socks.HandleConnection(ctx, conn, l.authenticator, l, M.Metadata{})
if err != nil {
return err
}
switch headerType {
case socks4.Version, socks5.Version:
return socks.HandleConnection0(ctx, conn, headerType, l.authenticator, l, M.Metadata{})
}
reader := std_bufio.NewReader(bufio.NewCachedReader(conn, buf.As([]byte{headerType})))
return http.HandleConnection(ctx, conn, reader, l.authenticator, l, M.Metadata{})
} }
func (l *ProxyListener) NewConnection(ctx context.Context, conn net.Conn, upstreamMetadata M.Metadata) error { func (l *ProxyListener) NewConnection(ctx context.Context, conn net.Conn, upstreamMetadata M.Metadata) error {

View File

@ -44,10 +44,10 @@ func NewTor(ctx context.Context, router adapter.Router, logger log.ContextLogger
startConf.ExtraArgs = options.ExtraArgs startConf.ExtraArgs = options.ExtraArgs
if options.DataDirectory != "" { if options.DataDirectory != "" {
dataDirAbs, _ := filepath.Abs(startConf.DataDir) dataDirAbs, _ := filepath.Abs(startConf.DataDir)
if geoIPPath := filepath.Join(dataDirAbs, "geoip"); rw.FileExists(geoIPPath) && !common.Contains(options.ExtraArgs, "--GeoIPFile") { if geoIPPath := filepath.Join(dataDirAbs, "geoip"); rw.IsFile(geoIPPath) && !common.Contains(options.ExtraArgs, "--GeoIPFile") {
options.ExtraArgs = append(options.ExtraArgs, "--GeoIPFile", geoIPPath) options.ExtraArgs = append(options.ExtraArgs, "--GeoIPFile", geoIPPath)
} }
if geoIP6Path := filepath.Join(dataDirAbs, "geoip6"); rw.FileExists(geoIP6Path) && !common.Contains(options.ExtraArgs, "--GeoIPv6File") { if geoIP6Path := filepath.Join(dataDirAbs, "geoip6"); rw.IsFile(geoIP6Path) && !common.Contains(options.ExtraArgs, "--GeoIPv6File") {
options.ExtraArgs = append(options.ExtraArgs, "--GeoIPv6File", geoIP6Path) options.ExtraArgs = append(options.ExtraArgs, "--GeoIPv6File", geoIP6Path)
} }
} }
@ -58,8 +58,12 @@ func NewTor(ctx context.Context, router adapter.Router, logger log.ContextLogger
} }
if startConf.DataDir != "" { if startConf.DataDir != "" {
torrcFile := filepath.Join(startConf.DataDir, "torrc") torrcFile := filepath.Join(startConf.DataDir, "torrc")
if !rw.FileExists(torrcFile) { err := rw.MkdirParent(torrcFile)
err := rw.WriteFile(torrcFile, []byte("")) if err != nil {
return nil, err
}
if !rw.IsFile(torrcFile) {
err := os.WriteFile(torrcFile, []byte(""), 0o600)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -82,7 +82,8 @@ type Router struct {
interfaceFinder *control.DefaultInterfaceFinder interfaceFinder *control.DefaultInterfaceFinder
autoDetectInterface bool autoDetectInterface bool
defaultInterface string defaultInterface string
defaultMark int defaultMark uint32
autoRedirectOutputMark uint32
networkMonitor tun.NetworkUpdateMonitor networkMonitor tun.NetworkUpdateMonitor
interfaceMonitor tun.DefaultInterfaceMonitor interfaceMonitor tun.DefaultInterfaceMonitor
packageManager tun.PackageManager packageManager tun.PackageManager
@ -533,7 +534,10 @@ func (r *Router) Start() error {
if r.needPackageManager && r.platformInterface == nil { if r.needPackageManager && r.platformInterface == nil {
monitor.Start("initialize package manager") monitor.Start("initialize package manager")
packageManager, err := tun.NewPackageManager(r) packageManager, err := tun.NewPackageManager(tun.PackageManagerOptions{
Callback: r,
Logger: r.logger,
})
monitor.Finish() monitor.Finish()
if err != nil { if err != nil {
return E.Cause(err, "create package manager") return E.Cause(err, "create package manager")
@ -724,10 +728,26 @@ func (r *Router) PostStart() error {
return E.Cause(err, "initialize rule[", i, "]") return E.Cause(err, "initialize rule[", i, "]")
} }
} }
for _, ruleSet := range r.ruleSets {
monitor.Start("post start rule_set[", ruleSet.Name(), "]")
err := ruleSet.PostStart()
monitor.Finish()
if err != nil {
return E.Cause(err, "post start rule_set[", ruleSet.Name(), "]")
}
}
r.started = true r.started = true
return nil return nil
} }
func (r *Router) Cleanup() error {
for _, ruleSet := range r.ruleSetMap {
ruleSet.Cleanup()
}
runtime.GC()
return nil
}
func (r *Router) Outbound(tag string) (adapter.Outbound, bool) { func (r *Router) Outbound(tag string) (adapter.Outbound, bool) {
outbound, loaded := r.outboundByTag[tag] outbound, loaded := r.outboundByTag[tag]
return outbound, loaded return outbound, loaded
@ -822,7 +842,16 @@ func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata ad
if metadata.InboundOptions.SniffEnabled { if metadata.InboundOptions.SniffEnabled {
buffer := buf.NewPacket() buffer := buf.NewPacket()
sniffMetadata, err := sniff.PeekStream(ctx, conn, buffer, time.Duration(metadata.InboundOptions.SniffTimeout), sniff.StreamDomainNameQuery, sniff.TLSClientHello, sniff.HTTPHost) sniffMetadata, err := sniff.PeekStream(
ctx,
conn,
buffer,
time.Duration(metadata.InboundOptions.SniffTimeout),
sniff.StreamDomainNameQuery,
sniff.TLSClientHello,
sniff.HTTPHost,
sniff.BitTorrent,
)
if sniffMetadata != nil { if sniffMetadata != nil {
metadata.Protocol = sniffMetadata.Protocol metadata.Protocol = sniffMetadata.Protocol
metadata.Domain = sniffMetadata.Domain metadata.Domain = sniffMetadata.Domain
@ -949,7 +978,16 @@ func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, m
metadata.Destination = destination metadata.Destination = destination
} }
if metadata.InboundOptions.SniffEnabled { if metadata.InboundOptions.SniffEnabled {
sniffMetadata, _ := sniff.PeekPacket(ctx, buffer.Bytes(), sniff.DomainNameQuery, sniff.QUICClientHello, sniff.STUNMessage) sniffMetadata, _ := sniff.PeekPacket(
ctx,
buffer.Bytes(),
sniff.DomainNameQuery,
sniff.QUICClientHello,
sniff.STUNMessage,
sniff.UTP,
sniff.UDPTracker,
sniff.DTLSRecord,
)
if sniffMetadata != nil { if sniffMetadata != nil {
metadata.Protocol = sniffMetadata.Protocol metadata.Protocol = sniffMetadata.Protocol
metadata.Domain = sniffMetadata.Domain metadata.Domain = sniffMetadata.Domain
@ -1114,11 +1152,23 @@ func (r *Router) AutoDetectInterfaceFunc() control.Func {
} }
} }
func (r *Router) RegisterAutoRedirectOutputMark(mark uint32) error {
if r.autoRedirectOutputMark > 0 {
return E.New("only one auto-redirect can be configured")
}
r.autoRedirectOutputMark = mark
return nil
}
func (r *Router) AutoRedirectOutputMark() uint32 {
return r.autoRedirectOutputMark
}
func (r *Router) DefaultInterface() string { func (r *Router) DefaultInterface() string {
return r.defaultInterface return r.defaultInterface
} }
func (r *Router) DefaultMark() int { func (r *Router) DefaultMark() uint32 {
return r.defaultMark return r.defaultMark
} }

View File

@ -8,7 +8,6 @@ import (
"time" "time"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-dns" "github.com/sagernet/sing-dns"
"github.com/sagernet/sing/common/cache" "github.com/sagernet/sing/common/cache"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
@ -105,7 +104,8 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, er
response, cached = r.dnsClient.ExchangeCache(ctx, message) response, cached = r.dnsClient.ExchangeCache(ctx, message)
if !cached { if !cached {
var metadata *adapter.InboundContext var metadata *adapter.InboundContext
ctx, metadata = adapter.AppendContext(ctx) ctx, metadata = adapter.ExtendContext(ctx)
metadata.Destination = M.Socksaddr{}
if len(message.Question) > 0 { if len(message.Question) > 0 {
metadata.QueryType = message.Question[0].Qtype metadata.QueryType = message.Question[0].Qtype
switch metadata.QueryType { switch metadata.QueryType {
@ -125,23 +125,24 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, er
for { for {
var ( var (
dnsCtx context.Context dnsCtx context.Context
cancel context.CancelFunc
addressLimit bool addressLimit bool
) )
dnsCtx, transport, strategy, rule, ruleIndex = r.matchDNS(ctx, true, ruleIndex, isAddressQuery(message)) dnsCtx, transport, strategy, rule, ruleIndex = r.matchDNS(ctx, true, ruleIndex, isAddressQuery(message))
dnsCtx, cancel = context.WithTimeout(dnsCtx, C.DNSTimeout) dnsCtx = adapter.OverrideContext(dnsCtx)
if rule != nil && rule.WithAddressLimit() { if rule != nil && rule.WithAddressLimit() {
addressLimit = true addressLimit = true
response, err = r.dnsClient.ExchangeWithResponseCheck(dnsCtx, transport, message, strategy, func(response *mDNS.Msg) bool { response, err = r.dnsClient.ExchangeWithResponseCheck(dnsCtx, transport, message, strategy, func(response *mDNS.Msg) bool {
metadata.DestinationAddresses, _ = dns.MessageToAddresses(response) addresses, addrErr := dns.MessageToAddresses(response)
if addrErr != nil {
return false
}
metadata.DestinationAddresses = addresses
return rule.MatchAddressLimit(metadata) return rule.MatchAddressLimit(metadata)
}) })
} else { } else {
addressLimit = false addressLimit = false
response, err = r.dnsClient.Exchange(dnsCtx, transport, message, strategy) response, err = r.dnsClient.Exchange(dnsCtx, transport, message, strategy)
} }
cancel()
var rejected bool var rejected bool
if err != nil { if err != nil {
if errors.Is(err, dns.ErrResponseRejectedCached) { if errors.Is(err, dns.ErrResponseRejectedCached) {
@ -191,7 +192,8 @@ func (r *Router) Lookup(ctx context.Context, domain string, strategy dns.DomainS
return responseAddrs, nil return responseAddrs, nil
} }
r.dnsLogger.DebugContext(ctx, "lookup domain ", domain) r.dnsLogger.DebugContext(ctx, "lookup domain ", domain)
ctx, metadata := adapter.AppendContext(ctx) ctx, metadata := adapter.ExtendContext(ctx)
metadata.Destination = M.Socksaddr{}
metadata.Domain = domain metadata.Domain = domain
var ( var (
transport dns.Transport transport dns.Transport
@ -203,16 +205,13 @@ func (r *Router) Lookup(ctx context.Context, domain string, strategy dns.DomainS
for { for {
var ( var (
dnsCtx context.Context dnsCtx context.Context
cancel context.CancelFunc
addressLimit bool addressLimit bool
) )
metadata.ResetRuleCache()
metadata.DestinationAddresses = nil
dnsCtx, transport, transportStrategy, rule, ruleIndex = r.matchDNS(ctx, false, ruleIndex, true) dnsCtx, transport, transportStrategy, rule, ruleIndex = r.matchDNS(ctx, false, ruleIndex, true)
dnsCtx = adapter.OverrideContext(dnsCtx)
if strategy == dns.DomainStrategyAsIS { if strategy == dns.DomainStrategyAsIS {
strategy = transportStrategy strategy = transportStrategy
} }
dnsCtx, cancel = context.WithTimeout(dnsCtx, C.DNSTimeout)
if rule != nil && rule.WithAddressLimit() { if rule != nil && rule.WithAddressLimit() {
addressLimit = true addressLimit = true
responseAddrs, err = r.dnsClient.LookupWithResponseCheck(dnsCtx, transport, domain, strategy, func(responseAddrs []netip.Addr) bool { responseAddrs, err = r.dnsClient.LookupWithResponseCheck(dnsCtx, transport, domain, strategy, func(responseAddrs []netip.Addr) bool {
@ -223,7 +222,6 @@ func (r *Router) Lookup(ctx context.Context, domain string, strategy dns.DomainS
addressLimit = false addressLimit = false
responseAddrs, err = r.dnsClient.Lookup(dnsCtx, transport, domain, strategy) responseAddrs, err = r.dnsClient.Lookup(dnsCtx, transport, domain, strategy)
} }
cancel()
if err != nil { if err != nil {
if errors.Is(err, dns.ErrResponseRejectedCached) { if errors.Is(err, dns.ErrResponseRejectedCached) {
r.dnsLogger.DebugContext(ctx, "response rejected for ", domain, " (cached)") r.dnsLogger.DebugContext(ctx, "response rejected for ", domain, " (cached)")

View File

@ -50,7 +50,7 @@ func (r *Router) prepareGeoIPDatabase() error {
geoPath = foundPath geoPath = foundPath
} }
} }
if !rw.FileExists(geoPath) { if !rw.IsFile(geoPath) {
geoPath = filemanager.BasePath(r.ctx, geoPath) geoPath = filemanager.BasePath(r.ctx, geoPath)
} }
if stat, err := os.Stat(geoPath); err == nil { if stat, err := os.Stat(geoPath); err == nil {
@ -61,7 +61,7 @@ func (r *Router) prepareGeoIPDatabase() error {
os.Remove(geoPath) os.Remove(geoPath)
} }
} }
if !rw.FileExists(geoPath) { if !rw.IsFile(geoPath) {
r.logger.Warn("geoip database not exists: ", geoPath) r.logger.Warn("geoip database not exists: ", geoPath)
var err error var err error
for attempts := 0; attempts < 3; attempts++ { for attempts := 0; attempts < 3; attempts++ {
@ -96,7 +96,7 @@ func (r *Router) prepareGeositeDatabase() error {
geoPath = foundPath geoPath = foundPath
} }
} }
if !rw.FileExists(geoPath) { if !rw.IsFile(geoPath) {
geoPath = filemanager.BasePath(r.ctx, geoPath) geoPath = filemanager.BasePath(r.ctx, geoPath)
} }
if stat, err := os.Stat(geoPath); err == nil { if stat, err := os.Stat(geoPath); err == nil {
@ -107,7 +107,7 @@ func (r *Router) prepareGeositeDatabase() error {
os.Remove(geoPath) os.Remove(geoPath)
} }
} }
if !rw.FileExists(geoPath) { if !rw.IsFile(geoPath) {
r.logger.Warn("geosite database not exists: ", geoPath) r.logger.Warn("geosite database not exists: ", geoPath)
var err error var err error
for attempts := 0; attempts < 3; attempts++ { for attempts := 0; attempts < 3; attempts++ {

View File

@ -29,9 +29,13 @@ func (r *abstractDefaultRule) Type() string {
func (r *abstractDefaultRule) Start() error { func (r *abstractDefaultRule) Start() error {
for _, item := range r.allItems { for _, item := range r.allItems {
err := common.Start(item) if starter, isStarter := item.(interface {
if err != nil { Start() error
return err }); isStarter {
err := starter.Start()
if err != nil {
return err
}
} }
} }
return nil return nil
@ -183,8 +187,13 @@ func (r *abstractLogicalRule) UpdateGeosite() error {
} }
func (r *abstractLogicalRule) Start() error { func (r *abstractLogicalRule) Start() error {
for _, rule := range common.FilterIsInstance(r.rules, func(it adapter.HeadlessRule) (common.Starter, bool) { for _, rule := range common.FilterIsInstance(r.rules, func(it adapter.HeadlessRule) (interface {
rule, loaded := it.(common.Starter) Start() error
}, bool,
) {
rule, loaded := it.(interface {
Start() error
})
return rule, loaded return rule, loaded
}) { }) {
err := rule.Start() err := rule.Start()

View File

@ -205,7 +205,7 @@ func NewDefaultRule(router adapter.Router, logger log.ContextLogger, options opt
rule.allItems = append(rule.allItems, item) rule.allItems = append(rule.allItems, item)
} }
if len(options.RuleSet) > 0 { if len(options.RuleSet) > 0 {
item := NewRuleSetItem(router, options.RuleSet, options.RuleSetIPCIDRMatchSource) item := NewRuleSetItem(router, options.RuleSet, options.RuleSetIPCIDRMatchSource, false)
rule.items = append(rule.items, item) rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item) rule.allItems = append(rule.allItems, item)
} }

View File

@ -219,7 +219,7 @@ func NewDefaultDNSRule(router adapter.Router, logger log.ContextLogger, options
rule.allItems = append(rule.allItems, item) rule.allItems = append(rule.allItems, item)
} }
if len(options.RuleSet) > 0 { if len(options.RuleSet) > 0 {
item := NewRuleSetItem(router, options.RuleSet, options.RuleSetIPCIDRMatchSource) item := NewRuleSetItem(router, options.RuleSet, options.RuleSetIPCIDRMatchSource, options.RuleSetIPCIDRAcceptEmpty)
rule.items = append(rule.items, item) rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item) rule.allItems = append(rule.allItems, item)
} }

View File

@ -75,18 +75,19 @@ func NewRawIPCIDRItem(isSource bool, ipSet *netipx.IPSet) *IPCIDRItem {
func (r *IPCIDRItem) Match(metadata *adapter.InboundContext) bool { func (r *IPCIDRItem) Match(metadata *adapter.InboundContext) bool {
if r.isSource || metadata.IPCIDRMatchSource { if r.isSource || metadata.IPCIDRMatchSource {
return r.ipSet.Contains(metadata.Source.Addr) return r.ipSet.Contains(metadata.Source.Addr)
} else { }
if metadata.Destination.IsIP() { if metadata.Destination.IsIP() {
return r.ipSet.Contains(metadata.Destination.Addr) return r.ipSet.Contains(metadata.Destination.Addr)
} else { }
for _, address := range metadata.DestinationAddresses { if len(metadata.DestinationAddresses) > 0 {
if r.ipSet.Contains(address) { for _, address := range metadata.DestinationAddresses {
return true if r.ipSet.Contains(address) {
} return true
} }
} }
return false
} }
return false return metadata.IPCIDRAcceptEmpty
} }
func (r *IPCIDRItem) String() string { func (r *IPCIDRItem) String() string {

View File

@ -15,14 +15,16 @@ type RuleSetItem struct {
router adapter.Router router adapter.Router
tagList []string tagList []string
setList []adapter.RuleSet setList []adapter.RuleSet
ipcidrMatchSource bool ipCidrMatchSource bool
ipCidrAcceptEmpty bool
} }
func NewRuleSetItem(router adapter.Router, tagList []string, ipCIDRMatchSource bool) *RuleSetItem { func NewRuleSetItem(router adapter.Router, tagList []string, ipCIDRMatchSource bool, ipCidrAcceptEmpty bool) *RuleSetItem {
return &RuleSetItem{ return &RuleSetItem{
router: router, router: router,
tagList: tagList, tagList: tagList,
ipcidrMatchSource: ipCIDRMatchSource, ipCidrMatchSource: ipCIDRMatchSource,
ipCidrAcceptEmpty: ipCidrAcceptEmpty,
} }
} }
@ -32,13 +34,15 @@ func (r *RuleSetItem) Start() error {
if !loaded { if !loaded {
return E.New("rule-set not found: ", tag) return E.New("rule-set not found: ", tag)
} }
ruleSet.IncRef()
r.setList = append(r.setList, ruleSet) r.setList = append(r.setList, ruleSet)
} }
return nil return nil
} }
func (r *RuleSetItem) Match(metadata *adapter.InboundContext) bool { func (r *RuleSetItem) Match(metadata *adapter.InboundContext) bool {
metadata.IPCIDRMatchSource = r.ipcidrMatchSource metadata.IPCIDRMatchSource = r.ipCidrMatchSource
metadata.IPCIDRAcceptEmpty = r.ipCidrAcceptEmpty
for _, ruleSet := range r.setList { for _, ruleSet := range r.setList {
if ruleSet.Match(metadata) { if ruleSet.Match(metadata) {
return true return true
@ -48,7 +52,7 @@ func (r *RuleSetItem) Match(metadata *adapter.InboundContext) bool {
} }
func (r *RuleSetItem) ContainsDestinationIPCIDRRule() bool { func (r *RuleSetItem) ContainsDestinationIPCIDRRule() bool {
if r.ipcidrMatchSource { if r.ipCidrMatchSource {
return false return false
} }
return common.Any(r.setList, func(ruleSet adapter.RuleSet) bool { return common.Any(r.setList, func(ruleSet adapter.RuleSet) bool {

View File

@ -9,20 +9,41 @@ import (
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger" "github.com/sagernet/sing/common/logger"
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"
"go4.org/netipx"
) )
func NewRuleSet(ctx context.Context, router adapter.Router, logger logger.ContextLogger, options option.RuleSet) (adapter.RuleSet, error) { func NewRuleSet(ctx context.Context, router adapter.Router, logger logger.ContextLogger, options option.RuleSet) (adapter.RuleSet, error) {
switch options.Type { switch options.Type {
case C.RuleSetTypeLocal: case C.RuleSetTypeInline, C.RuleSetTypeLocal, "":
return NewLocalRuleSet(router, options) return NewLocalRuleSet(router, logger, options)
case C.RuleSetTypeRemote: case C.RuleSetTypeRemote:
return NewRemoteRuleSet(ctx, router, logger, options), nil return NewRemoteRuleSet(ctx, router, logger, options), nil
default: default:
return nil, E.New("unknown rule set type: ", options.Type) return nil, E.New("unknown rule-set type: ", options.Type)
}
}
func extractIPSetFromRule(rawRule adapter.HeadlessRule) []*netipx.IPSet {
switch rule := rawRule.(type) {
case *DefaultHeadlessRule:
return common.FlatMap(rule.destinationIPCIDRItems, func(rawItem RuleItem) []*netipx.IPSet {
switch item := rawItem.(type) {
case *IPCIDRItem:
return []*netipx.IPSet{item.ipSet}
default:
return nil
}
})
case *LogicalHeadlessRule:
return common.FlatMap(rule.rules, extractIPSetFromRule)
default:
panic("unexpected rule type")
} }
} }

View File

@ -3,62 +3,184 @@ package route
import ( import (
"context" "context"
"os" "os"
"path/filepath"
"strings" "strings"
"github.com/sagernet/fswatch"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/srs" "github.com/sagernet/sing-box/common/srs"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/atomic"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format" F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/logger"
"github.com/sagernet/sing/common/x/list"
"go4.org/netipx"
) )
var _ adapter.RuleSet = (*LocalRuleSet)(nil) var _ adapter.RuleSet = (*LocalRuleSet)(nil)
type LocalRuleSet struct { type LocalRuleSet struct {
rules []adapter.HeadlessRule router adapter.Router
metadata adapter.RuleSetMetadata logger logger.Logger
tag string
rules []adapter.HeadlessRule
metadata adapter.RuleSetMetadata
fileFormat string
watcher *fswatch.Watcher
refs atomic.Int32
} }
func NewLocalRuleSet(router adapter.Router, options option.RuleSet) (*LocalRuleSet, error) { func NewLocalRuleSet(router adapter.Router, logger logger.Logger, options option.RuleSet) (*LocalRuleSet, error) {
var plainRuleSet option.PlainRuleSet ruleSet := &LocalRuleSet{
switch options.Format { router: router,
case C.RuleSetFormatSource, "": logger: logger,
content, err := os.ReadFile(options.LocalOptions.Path) tag: options.Tag,
fileFormat: options.Format,
}
if options.Type == C.RuleSetTypeInline {
if len(options.InlineOptions.Rules) == 0 {
return nil, E.New("empty inline rule-set")
}
err := ruleSet.reloadRules(options.InlineOptions.Rules)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} else {
err := ruleSet.reloadFile(options.LocalOptions.Path)
if err != nil {
return nil, err
}
}
if options.Type == C.RuleSetTypeLocal {
var watcher *fswatch.Watcher
filePath, _ := filepath.Abs(options.LocalOptions.Path)
watcher, err := fswatch.NewWatcher(fswatch.Options{
Path: []string{filePath},
Callback: func(path string) {
uErr := ruleSet.reloadFile(path)
if uErr != nil {
logger.Error(E.Cause(uErr, "reload rule-set ", options.Tag))
}
},
})
if err != nil {
return nil, err
}
ruleSet.watcher = watcher
}
return ruleSet, nil
}
func (s *LocalRuleSet) Name() string {
return s.tag
}
func (s *LocalRuleSet) String() string {
return strings.Join(F.MapToString(s.rules), " ")
}
func (s *LocalRuleSet) StartContext(ctx context.Context, startContext adapter.RuleSetStartContext) error {
if s.watcher != nil {
err := s.watcher.Start()
if err != nil {
s.logger.Error(E.Cause(err, "watch rule-set file"))
}
}
return nil
}
func (s *LocalRuleSet) reloadFile(path string) error {
var plainRuleSet option.PlainRuleSet
switch s.fileFormat {
case C.RuleSetFormatSource, "":
content, err := os.ReadFile(path)
if err != nil {
return err
}
compat, err := json.UnmarshalExtended[option.PlainRuleSetCompat](content) compat, err := json.UnmarshalExtended[option.PlainRuleSetCompat](content)
if err != nil { if err != nil {
return nil, err return err
} }
plainRuleSet = compat.Upgrade() plainRuleSet, err = compat.Upgrade()
case C.RuleSetFormatBinary:
setFile, err := os.Open(options.LocalOptions.Path)
if err != nil { if err != nil {
return nil, err return err
}
case C.RuleSetFormatBinary:
setFile, err := os.Open(path)
if err != nil {
return err
} }
plainRuleSet, err = srs.Read(setFile, false) plainRuleSet, err = srs.Read(setFile, false)
if err != nil { if err != nil {
return nil, err return err
} }
default: default:
return nil, E.New("unknown rule set format: ", options.Format) return E.New("unknown rule-set format: ", s.fileFormat)
} }
rules := make([]adapter.HeadlessRule, len(plainRuleSet.Rules)) return s.reloadRules(plainRuleSet.Rules)
}
func (s *LocalRuleSet) reloadRules(headlessRules []option.HeadlessRule) error {
rules := make([]adapter.HeadlessRule, len(headlessRules))
var err error var err error
for i, ruleOptions := range plainRuleSet.Rules { for i, ruleOptions := range headlessRules {
rules[i], err = NewHeadlessRule(router, ruleOptions) rules[i], err = NewHeadlessRule(s.router, ruleOptions)
if err != nil { if err != nil {
return nil, E.Cause(err, "parse rule_set.rules.[", i, "]") return E.Cause(err, "parse rule_set.rules.[", i, "]")
} }
} }
var metadata adapter.RuleSetMetadata var metadata adapter.RuleSetMetadata
metadata.ContainsProcessRule = hasHeadlessRule(plainRuleSet.Rules, isProcessHeadlessRule) metadata.ContainsProcessRule = hasHeadlessRule(headlessRules, isProcessHeadlessRule)
metadata.ContainsWIFIRule = hasHeadlessRule(plainRuleSet.Rules, isWIFIHeadlessRule) metadata.ContainsWIFIRule = hasHeadlessRule(headlessRules, isWIFIHeadlessRule)
metadata.ContainsIPCIDRRule = hasHeadlessRule(plainRuleSet.Rules, isIPCIDRHeadlessRule) metadata.ContainsIPCIDRRule = hasHeadlessRule(headlessRules, isIPCIDRHeadlessRule)
return &LocalRuleSet{rules, metadata}, nil s.rules = rules
s.metadata = metadata
return nil
}
func (s *LocalRuleSet) PostStart() error {
return nil
}
func (s *LocalRuleSet) Metadata() adapter.RuleSetMetadata {
return s.metadata
}
func (s *LocalRuleSet) ExtractIPSet() []*netipx.IPSet {
return common.FlatMap(s.rules, extractIPSetFromRule)
}
func (s *LocalRuleSet) IncRef() {
s.refs.Add(1)
}
func (s *LocalRuleSet) DecRef() {
if s.refs.Add(-1) < 0 {
panic("rule-set: negative refs")
}
}
func (s *LocalRuleSet) Cleanup() {
if s.refs.Load() == 0 {
s.rules = nil
}
}
func (s *LocalRuleSet) RegisterCallback(callback adapter.RuleSetUpdateCallback) *list.Element[adapter.RuleSetUpdateCallback] {
return nil
}
func (s *LocalRuleSet) UnregisterCallback(element *list.Element[adapter.RuleSetUpdateCallback]) {
}
func (s *LocalRuleSet) Close() error {
s.rules = nil
return common.Close(common.PtrOrNil(s.watcher))
} }
func (s *LocalRuleSet) Match(metadata *adapter.InboundContext) bool { func (s *LocalRuleSet) Match(metadata *adapter.InboundContext) bool {
@ -69,19 +191,3 @@ func (s *LocalRuleSet) Match(metadata *adapter.InboundContext) bool {
} }
return false return false
} }
func (s *LocalRuleSet) String() string {
return strings.Join(F.MapToString(s.rules), " ")
}
func (s *LocalRuleSet) StartContext(ctx context.Context, startContext adapter.RuleSetStartContext) error {
return nil
}
func (s *LocalRuleSet) Metadata() adapter.RuleSetMetadata {
return s.metadata
}
func (s *LocalRuleSet) Close() error {
return nil
}

View File

@ -8,20 +8,26 @@ import (
"net/http" "net/http"
"runtime" "runtime"
"strings" "strings"
"sync"
"time" "time"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/srs" "github.com/sagernet/sing-box/common/srs"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/atomic"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format" F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/logger" "github.com/sagernet/sing/common/logger"
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"
"github.com/sagernet/sing/common/x/list"
"github.com/sagernet/sing/service" "github.com/sagernet/sing/service"
"github.com/sagernet/sing/service/pause" "github.com/sagernet/sing/service/pause"
"go4.org/netipx"
) )
var _ adapter.RuleSet = (*RemoteRuleSet)(nil) var _ adapter.RuleSet = (*RemoteRuleSet)(nil)
@ -40,6 +46,9 @@ type RemoteRuleSet struct {
lastEtag string lastEtag string
updateTicker *time.Ticker updateTicker *time.Ticker
pauseManager pause.Manager pauseManager pause.Manager
callbackAccess sync.Mutex
callbacks list.List[adapter.RuleSetUpdateCallback]
refs atomic.Int32
} }
func NewRemoteRuleSet(ctx context.Context, router adapter.Router, logger logger.ContextLogger, options option.RuleSet) *RemoteRuleSet { func NewRemoteRuleSet(ctx context.Context, router adapter.Router, logger logger.ContextLogger, options option.RuleSet) *RemoteRuleSet {
@ -61,13 +70,8 @@ func NewRemoteRuleSet(ctx context.Context, router adapter.Router, logger logger.
} }
} }
func (s *RemoteRuleSet) Match(metadata *adapter.InboundContext) bool { func (s *RemoteRuleSet) Name() string {
for _, rule := range s.rules { return s.options.Tag
if rule.Match(metadata) {
return true
}
}
return false
} }
func (s *RemoteRuleSet) String() string { func (s *RemoteRuleSet) String() string {
@ -108,6 +112,10 @@ func (s *RemoteRuleSet) StartContext(ctx context.Context, startContext adapter.R
} }
} }
s.updateTicker = time.NewTicker(s.updateInterval) s.updateTicker = time.NewTicker(s.updateInterval)
return nil
}
func (s *RemoteRuleSet) PostStart() error {
go s.loopUpdate() go s.loopUpdate()
return nil return nil
} }
@ -116,6 +124,38 @@ func (s *RemoteRuleSet) Metadata() adapter.RuleSetMetadata {
return s.metadata return s.metadata
} }
func (s *RemoteRuleSet) ExtractIPSet() []*netipx.IPSet {
return common.FlatMap(s.rules, extractIPSetFromRule)
}
func (s *RemoteRuleSet) IncRef() {
s.refs.Add(1)
}
func (s *RemoteRuleSet) DecRef() {
if s.refs.Add(-1) < 0 {
panic("rule-set: negative refs")
}
}
func (s *RemoteRuleSet) Cleanup() {
if s.refs.Load() == 0 {
s.rules = nil
}
}
func (s *RemoteRuleSet) RegisterCallback(callback adapter.RuleSetUpdateCallback) *list.Element[adapter.RuleSetUpdateCallback] {
s.callbackAccess.Lock()
defer s.callbackAccess.Unlock()
return s.callbacks.PushBack(callback)
}
func (s *RemoteRuleSet) UnregisterCallback(element *list.Element[adapter.RuleSetUpdateCallback]) {
s.callbackAccess.Lock()
defer s.callbackAccess.Unlock()
s.callbacks.Remove(element)
}
func (s *RemoteRuleSet) loadBytes(content []byte) error { func (s *RemoteRuleSet) loadBytes(content []byte) error {
var ( var (
plainRuleSet option.PlainRuleSet plainRuleSet option.PlainRuleSet
@ -128,14 +168,17 @@ func (s *RemoteRuleSet) loadBytes(content []byte) error {
if err != nil { if err != nil {
return err return err
} }
plainRuleSet = compat.Upgrade() plainRuleSet, err = compat.Upgrade()
if err != nil {
return err
}
case C.RuleSetFormatBinary: case C.RuleSetFormatBinary:
plainRuleSet, err = srs.Read(bytes.NewReader(content), false) plainRuleSet, err = srs.Read(bytes.NewReader(content), false)
if err != nil { if err != nil {
return err return err
} }
default: default:
return E.New("unknown rule set format: ", s.options.Format) return E.New("unknown rule-set format: ", s.options.Format)
} }
rules := make([]adapter.HeadlessRule, len(plainRuleSet.Rules)) rules := make([]adapter.HeadlessRule, len(plainRuleSet.Rules))
for i, ruleOptions := range plainRuleSet.Rules { for i, ruleOptions := range plainRuleSet.Rules {
@ -148,6 +191,12 @@ func (s *RemoteRuleSet) loadBytes(content []byte) error {
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()
callbacks := s.callbacks.Array()
s.callbackAccess.Unlock()
for _, callback := range callbacks {
callback(s)
}
return nil return nil
} }
@ -156,6 +205,8 @@ func (s *RemoteRuleSet) loopUpdate() {
err := s.fetchOnce(s.ctx, nil) err := s.fetchOnce(s.ctx, nil)
if err != nil { if err != nil {
s.logger.Error("fetch rule-set ", s.options.Tag, ": ", err) s.logger.Error("fetch rule-set ", s.options.Tag, ": ", err)
} else if s.refs.Load() == 0 {
s.rules = nil
} }
} }
for { for {
@ -168,6 +219,8 @@ func (s *RemoteRuleSet) loopUpdate() {
err := s.fetchOnce(s.ctx, nil) err := s.fetchOnce(s.ctx, nil)
if err != nil { if err != nil {
s.logger.Error("fetch rule-set ", s.options.Tag, ": ", err) s.logger.Error("fetch rule-set ", s.options.Tag, ": ", err)
} else if s.refs.Load() == 0 {
s.rules = nil
} }
} }
} }
@ -253,7 +306,17 @@ func (s *RemoteRuleSet) fetchOnce(ctx context.Context, startContext adapter.Rule
} }
func (s *RemoteRuleSet) Close() error { func (s *RemoteRuleSet) Close() error {
s.rules = nil
s.updateTicker.Stop() s.updateTicker.Stop()
s.cancel() s.cancel()
return nil return nil
} }
func (s *RemoteRuleSet) Match(metadata *adapter.InboundContext) bool {
for _, rule := range s.rules {
if rule.Match(metadata) {
return true
}
}
return false
}

View File

@ -1,12 +1,14 @@
package trojan package trojan
import ( import (
std_bufio "bufio"
"context" "context"
"net" "net"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/bufio"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/common/rw"
"github.com/sagernet/sing/common/task" "github.com/sagernet/sing/common/task"
"github.com/sagernet/smux" "github.com/sagernet/smux"
) )
@ -33,27 +35,36 @@ func HandleMuxConnection(ctx context.Context, conn net.Conn, metadata M.Metadata
return group.Run(ctx) return group.Run(ctx)
} }
func newMuxConnection(ctx context.Context, stream net.Conn, metadata M.Metadata, handler Handler) { func newMuxConnection(ctx context.Context, conn net.Conn, metadata M.Metadata, handler Handler) {
err := newMuxConnection0(ctx, stream, metadata, handler) err := newMuxConnection0(ctx, conn, metadata, handler)
if err != nil { if err != nil {
handler.NewError(ctx, E.Cause(err, "process trojan-go multiplex connection")) handler.NewError(ctx, E.Cause(err, "process trojan-go multiplex connection"))
} }
} }
func newMuxConnection0(ctx context.Context, stream net.Conn, metadata M.Metadata, handler Handler) error { func newMuxConnection0(ctx context.Context, conn net.Conn, metadata M.Metadata, handler Handler) error {
command, err := rw.ReadByte(stream) reader := std_bufio.NewReader(conn)
command, err := reader.ReadByte()
if err != nil { if err != nil {
return E.Cause(err, "read command") return E.Cause(err, "read command")
} }
metadata.Destination, err = M.SocksaddrSerializer.ReadAddrPort(stream) metadata.Destination, err = M.SocksaddrSerializer.ReadAddrPort(reader)
if err != nil { if err != nil {
return E.Cause(err, "read destination") return E.Cause(err, "read destination")
} }
if reader.Buffered() > 0 {
buffer := buf.NewSize(reader.Buffered())
_, err = buffer.ReadFullFrom(reader, buffer.Len())
if err != nil {
return err
}
conn = bufio.NewCachedConn(conn, buffer)
}
switch command { switch command {
case CommandTCP: case CommandTCP:
return handler.NewConnection(ctx, stream, metadata) return handler.NewConnection(ctx, conn, metadata)
case CommandUDP: case CommandUDP:
return handler.NewPacketConnection(ctx, &PacketConn{Conn: stream}, metadata) return handler.NewPacketConnection(ctx, &PacketConn{Conn: conn}, metadata)
default: default:
return E.New("unknown command ", command) return E.New("unknown command ", command)
} }

Some files were not shown because too many files have changed in this diff Show More