Compare commits

..

10 Commits

Author SHA1 Message Date
世界
b290d0ed32 documentation: Update changelog 2023-04-09 15:06:20 +08:00
世界
2afe662646 clash api: download clash-dashboard if external-ui directory is empty 2023-04-09 12:39:33 +08:00
世界
107a9a3b51 Fix read deadline implementation 2023-04-09 12:39:33 +08:00
世界
3d0c64f523 Replace usages of uber/atomic 2023-04-09 12:39:33 +08:00
世界
422ca34ac2 Fix timeout error check 2023-04-08 12:25:51 +08:00
世界
6d63f9255f documentation: Update changelog 2023-04-08 09:37:58 +08:00
世界
6f2cc9761d Add multi-peer support for wireguard outbound 2023-04-08 09:37:58 +08:00
世界
b484d9bca6 Add fakeip support 2023-04-08 09:37:58 +08:00
世界
58c4fd745a Add L3 routing support 2023-04-08 09:17:12 +08:00
世界
7d1e6affb3 Add dns reverse mapping 2023-04-08 09:17:03 +08:00
248 changed files with 4057 additions and 5267 deletions

View File

@@ -31,6 +31,12 @@ jobs:
uses: actions/setup-go@v4 uses: actions/setup-go@v4
with: with:
go-version: ${{ steps.version.outputs.go_version }} go-version: ${{ steps.version.outputs.go_version }}
- name: Cache go module
uses: actions/cache@v3
with:
path: |
~/go/pkg/mod
key: go-${{ hashFiles('**/go.sum') }}
- name: Add cache to Go proxy - name: Add cache to Go proxy
run: | run: |
version=`git rev-parse HEAD` version=`git rev-parse HEAD`
@@ -190,6 +196,12 @@ jobs:
uses: actions/setup-go@v4 uses: actions/setup-go@v4
with: with:
go-version: ${{ steps.version.outputs.go_version }} go-version: ${{ steps.version.outputs.go_version }}
- name: Cache go module
uses: actions/cache@v3
with:
path: |
~/go/pkg/mod
key: go-${{ hashFiles('**/go.sum') }}
- name: Build - name: Build
id: build id: build
run: make run: make

View File

@@ -31,6 +31,12 @@ jobs:
uses: actions/setup-go@v4 uses: actions/setup-go@v4
with: with:
go-version: ${{ steps.version.outputs.go_version }} go-version: ${{ steps.version.outputs.go_version }}
- name: Cache go module
uses: actions/cache@v3
with:
path: |
~/go/pkg/mod
key: go-${{ hashFiles('**/go.sum') }}
- name: golangci-lint - name: golangci-lint
uses: golangci/golangci-lint-action@v3 uses: golangci/golangci-lint-action@v3
with: with:

View File

@@ -14,7 +14,6 @@ builds:
tags: tags:
- with_gvisor - with_gvisor
- with_quic - with_quic
- with_dhcp
- with_wireguard - with_wireguard
- with_utls - with_utls
- with_reality_server - with_reality_server
@@ -49,7 +48,6 @@ builds:
tags: tags:
- with_gvisor - with_gvisor
- with_quic - with_quic
- with_dhcp
- with_wireguard - with_wireguard
- with_utls - with_utls
- with_clash_api - with_clash_api

View File

@@ -9,7 +9,7 @@ RUN set -ex \
&& apk add git build-base \ && apk add git build-base \
&& export COMMIT=$(git rev-parse --short HEAD) \ && export COMMIT=$(git rev-parse --short HEAD) \
&& export VERSION=$(go run ./cmd/internal/read_tag) \ && export VERSION=$(go run ./cmd/internal/read_tag) \
&& go build -v -trimpath -tags with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_reality_server,with_clash_api,with_acme \ && go build -v -trimpath -tags with_gvisor,with_quic,with_wireguard,with_utls,with_reality_server,with_clash_api,with_acme \
-o /go/bin/sing-box \ -o /go/bin/sing-box \
-ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" -s -w -buildid=" \ -ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" -s -w -buildid=" \
./cmd/sing-box ./cmd/sing-box

View File

@@ -1,6 +1,6 @@
NAME = sing-box NAME = sing-box
COMMIT = $(shell git rev-parse --short HEAD) COMMIT = $(shell git rev-parse --short HEAD)
TAGS ?= with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_reality_server,with_clash_api TAGS ?= with_gvisor,with_quic,with_wireguard,with_utls,with_reality_server,with_clash_api
TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_ech,with_utls,with_reality_server,with_shadowsocksr TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_ech,with_utls,with_reality_server,with_shadowsocksr
GOHOSTOS = $(shell go env GOHOSTOS) GOHOSTOS = $(shell go env GOHOSTOS)
@@ -22,7 +22,7 @@ install:
fmt: fmt:
@gofumpt -l -w . @gofumpt -l -w .
@gofmt -s -w . @gofmt -s -w .
@gci write --custom-order -s standard -s "prefix(github.com/sagernet/)" -s "default" . @gci write --custom-order -s "standard,prefix(github.com/sagernet/),default" .
fmt_install: fmt_install:
go install -v mvdan.cc/gofumpt@latest go install -v mvdan.cc/gofumpt@latest
@@ -48,14 +48,14 @@ proto_install:
go install -v google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest go install -v google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
snapshot: snapshot:
go run ./cmd/internal/build goreleaser release --clean --snapshot || exit 1 go run ./cmd/internal/build goreleaser release --rm-dist --snapshot || exit 1
mkdir dist/release mkdir dist/release
mv dist/*.tar.gz dist/*.zip dist/*.deb dist/*.rpm dist/release mv dist/*.tar.gz dist/*.zip dist/*.deb dist/*.rpm dist/release
ghr --delete --draft --prerelease -p 1 nightly dist/release ghr --delete --draft --prerelease -p 1 nightly dist/release
rm -r dist rm -r dist
release: release:
go run ./cmd/internal/build goreleaser release --clean --skip-publish || exit 1 go run ./cmd/internal/build goreleaser release --rm-dist --skip-publish || exit 1
mkdir dist/release mkdir dist/release
mv dist/*.tar.gz dist/*.zip dist/*.deb dist/*.rpm dist/release mv dist/*.tar.gz dist/*.zip dist/*.deb dist/*.rpm dist/release
ghr --delete --draft --prerelease -p 3 $(shell git describe --tags) dist/release ghr --delete --draft --prerelease -p 3 $(shell git describe --tags) dist/release
@@ -77,20 +77,13 @@ test_stdio:
go mod tidy && \ go mod tidy && \
go test -v -tags "$(TAGS_TEST),force_stdio" . go test -v -tags "$(TAGS_TEST),force_stdio" .
android:
go run ./cmd/internal/build_libbox -target android
ios:
go run ./cmd/internal/build_libbox -target ios
lib: lib:
go run ./cmd/internal/build_libbox -target android go run ./cmd/internal/build_libbox
go run ./cmd/internal/build_libbox -target ios
lib_install: lib_install:
go get -v -d go get -v -d
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.0.0-20230728014906-3de089147f59 go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.0.0-20221130124640-349ebaa752ca
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.0.0-20230728014906-3de089147f59 go install -v github.com/sagernet/gomobile/cmd/gobind@v0.0.0-20221130124640-349ebaa752ca
clean: clean:
rm -rf bin dist sing-box rm -rf bin dist sing-box

View File

@@ -8,10 +8,6 @@ The universal proxy platform.
https://sing-box.sagernet.org https://sing-box.sagernet.org
## Support
https://community.sagernet.org/c/sing-box/
## License ## License
``` ```

View File

@@ -31,16 +31,10 @@ type Tracker interface {
} }
type OutboundGroup interface { type OutboundGroup interface {
Outbound
Now() string Now() string
All() []string All() []string
} }
type URLTestGroup interface {
OutboundGroup
URLTest(ctx context.Context, url string) (map[string]uint16, error)
}
func OutboundTag(detour Outbound) string { func OutboundTag(detour Outbound) string {
if group, isGroup := detour.(OutboundGroup); isGroup { if group, isGroup := detour.(OutboundGroup); isGroup {
return group.Now() return group.Now()

View File

@@ -4,13 +4,12 @@ import (
"net/netip" "net/netip"
"github.com/sagernet/sing-dns" "github.com/sagernet/sing-dns"
"github.com/sagernet/sing/common/logger"
) )
type FakeIPStore interface { type FakeIPStore interface {
Service Service
Contains(address netip.Addr) bool Contains(address netip.Addr) bool
Create(domain string, isIPv6 bool) (netip.Addr, error) Create(domain string, strategy dns.DomainStrategy) (netip.Addr, error)
Lookup(address netip.Addr) (string, bool) Lookup(address netip.Addr) (string, bool)
Reset() error Reset() error
} }
@@ -19,13 +18,6 @@ type FakeIPStorage interface {
FakeIPMetadata() *FakeIPMetadata FakeIPMetadata() *FakeIPMetadata
FakeIPSaveMetadata(metadata *FakeIPMetadata) error FakeIPSaveMetadata(metadata *FakeIPMetadata) error
FakeIPStore(address netip.Addr, domain string) error FakeIPStore(address netip.Addr, domain string) error
FakeIPStoreAsync(address netip.Addr, domain string, logger logger.Logger)
FakeIPLoad(address netip.Addr) (string, bool) FakeIPLoad(address netip.Addr) (string, bool)
FakeIPLoadDomain(domain string, isIPv6 bool) (netip.Addr, bool)
FakeIPReset() error FakeIPReset() error
} }
type FakeIPTransport interface {
dns.Transport
Store() FakeIPStore
}

View File

@@ -46,7 +46,6 @@ type InboundContext struct {
SourceGeoIPCode string SourceGeoIPCode string
GeoIPCode string GeoIPCode string
ProcessInfo *process.Info ProcessInfo *process.Info
FakeIP bool
// dns cache // dns cache

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"net" "net"
"github.com/sagernet/sing-tun"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
) )
@@ -13,8 +14,12 @@ type Outbound interface {
Type() string Type() string
Tag() string Tag() string
Network() []string Network() []string
Dependencies() []string
N.Dialer N.Dialer
NewConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error NewConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error
NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
} }
type IPOutbound interface {
Outbound
NewIPConnection(ctx context.Context, conn tun.RouteContext, metadata InboundContext) (tun.DirectDestination, error)
}

View File

@@ -4,6 +4,12 @@ type PreStarter interface {
PreStart() error PreStart() error
} }
type PostStarter interface { func PreStart(starter any) error {
PostStart() error if preService, ok := starter.(PreStarter); ok {
err := preService.PreStart()
if err != nil {
return err
}
}
return nil
} }

View File

@@ -25,6 +25,9 @@ type Router interface {
RouteConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error RouteConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error
RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
RouteIPConnection(ctx context.Context, conn tun.RouteContext, metadata InboundContext) tun.RouteAction
NatRequired(outbound string) bool
GeoIPReader() *geoip.Reader GeoIPReader() *geoip.Reader
LoadGeosite(code string) (Rule, error) LoadGeosite(code string) (Rule, error)
@@ -34,7 +37,6 @@ type Router interface {
LookupDefault(ctx context.Context, domain string) ([]netip.Addr, error) LookupDefault(ctx context.Context, domain string) ([]netip.Addr, error)
InterfaceFinder() control.InterfaceFinder InterfaceFinder() control.InterfaceFinder
UpdateInterfaces() error
DefaultInterface() string DefaultInterface() string
AutoDetectInterface() bool AutoDetectInterface() bool
AutoDetectInterfaceFunc() control.Func AutoDetectInterfaceFunc() control.Func
@@ -42,7 +44,9 @@ type Router interface {
NetworkMonitor() tun.NetworkUpdateMonitor NetworkMonitor() tun.NetworkUpdateMonitor
InterfaceMonitor() tun.DefaultInterfaceMonitor InterfaceMonitor() tun.DefaultInterfaceMonitor
PackageManager() tun.PackageManager PackageManager() tun.PackageManager
Rules() []Rule Rules() []Rule
IPRules() []IPRule
TimeService TimeService
@@ -51,8 +55,6 @@ type Router interface {
V2RayServer() V2RayServer V2RayServer() V2RayServer
SetV2RayServer(server V2RayServer) SetV2RayServer(server V2RayServer)
ResetNetwork() error
} }
type routerContextKey struct{} type routerContextKey struct{}
@@ -84,6 +86,11 @@ type DNSRule interface {
RewriteTTL() *uint32 RewriteTTL() *uint32
} }
type IPRule interface {
Rule
Action() tun.ActionType
}
type InterfaceUpdateListener interface { type InterfaceUpdateListener interface {
InterfaceUpdated() error InterfaceUpdated() error
} }

51
box.go
View File

@@ -62,7 +62,6 @@ func New(options Options) (*Box, error) {
defaultLogWriter = io.Discard defaultLogWriter = io.Discard
} }
logFactory, err := log.New(log.Options{ logFactory, err := log.New(log.Options{
Context: ctx,
Options: common.PtrValueOrDefault(options.Log), Options: common.PtrValueOrDefault(options.Log),
Observable: needClashAPI, Observable: needClashAPI,
DefaultWriter: defaultLogWriter, DefaultWriter: defaultLogWriter,
@@ -134,16 +133,10 @@ func New(options Options) (*Box, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
if options.PlatformInterface != nil {
err = options.PlatformInterface.Initialize(ctx, router)
if err != nil {
return nil, E.Cause(err, "initialize platform interface")
}
}
preServices := make(map[string]adapter.Service) preServices := make(map[string]adapter.Service)
postServices := make(map[string]adapter.Service) postServices := make(map[string]adapter.Service)
if needClashAPI { if needClashAPI {
clashServer, err := experimental.NewClashServer(ctx, router, logFactory.(log.ObservableFactory), common.PtrValueOrDefault(options.Experimental.ClashAPI)) clashServer, err := experimental.NewClashServer(router, logFactory.(log.ObservableFactory), common.PtrValueOrDefault(options.Experimental.ClashAPI))
if err != nil { if err != nil {
return nil, E.Cause(err, "create clash api server") return nil, E.Cause(err, "create clash api server")
} }
@@ -211,17 +204,26 @@ func (s *Box) Start() error {
func (s *Box) preStart() error { func (s *Box) preStart() error {
for serviceName, service := range s.preServices { for serviceName, service := range s.preServices {
if preService, isPreService := service.(adapter.PreStarter); isPreService { s.logger.Trace("pre-start ", serviceName)
s.logger.Trace("pre-start ", serviceName) err := adapter.PreStart(service)
err := preService.PreStart() if err != nil {
if err != nil { return E.Cause(err, "pre-starting ", serviceName)
return E.Cause(err, "pre-starting ", serviceName)
}
} }
} }
err := s.startOutbounds() for i, out := range s.outbounds {
if err != nil { var tag string
return err if out.Tag() == "" {
tag = F.ToString(i)
} else {
tag = out.Tag()
}
if starter, isStarter := out.(common.Starter); isStarter {
s.logger.Trace("initializing outbound/", out.Type(), "[", tag, "]")
err := starter.Start()
if err != nil {
return E.Cause(err, "initialize outbound/", out.Type(), "[", tag, "]")
}
}
} }
return s.router.Start() return s.router.Start()
} }
@@ -251,26 +253,13 @@ func (s *Box) start() error {
return E.Cause(err, "initialize inbound/", in.Type(), "[", tag, "]") return E.Cause(err, "initialize inbound/", in.Type(), "[", tag, "]")
} }
} }
return nil
}
func (s *Box) postStart() error {
for serviceName, service := range s.postServices { for serviceName, service := range s.postServices {
s.logger.Trace("starting ", service) s.logger.Trace("starting ", service)
err := service.Start() err = service.Start()
if err != nil { if err != nil {
return E.Cause(err, "start ", serviceName) return E.Cause(err, "start ", serviceName)
} }
} }
for serviceName, service := range s.outbounds {
if lateService, isLateService := service.(adapter.PostStarter); isLateService {
s.logger.Trace("post-starting ", service)
err := lateService.PostStart()
if err != nil {
return E.Cause(err, "post-start ", serviceName)
}
}
}
return nil return nil
} }

View File

@@ -1,79 +0,0 @@
package box
import (
"strings"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
)
func (s *Box) startOutbounds() error {
outboundTags := make(map[adapter.Outbound]string)
outbounds := make(map[string]adapter.Outbound)
for i, outboundToStart := range s.outbounds {
var outboundTag string
if outboundToStart.Tag() == "" {
outboundTag = F.ToString(i)
} else {
outboundTag = outboundToStart.Tag()
}
if _, exists := outbounds[outboundTag]; exists {
return E.New("outbound tag ", outboundTag, " duplicated")
}
outboundTags[outboundToStart] = outboundTag
outbounds[outboundTag] = outboundToStart
}
started := make(map[string]bool)
for {
canContinue := false
startOne:
for _, outboundToStart := range s.outbounds {
outboundTag := outboundTags[outboundToStart]
if started[outboundTag] {
continue
}
dependencies := outboundToStart.Dependencies()
for _, dependency := range dependencies {
if !started[dependency] {
continue startOne
}
}
started[outboundTag] = true
canContinue = true
if starter, isStarter := outboundToStart.(common.Starter); isStarter {
s.logger.Trace("initializing outbound/", outboundToStart.Type(), "[", outboundTag, "]")
err := starter.Start()
if err != nil {
return E.Cause(err, "initialize outbound/", outboundToStart.Type(), "[", outboundTag, "]")
}
}
}
if len(started) == len(s.outbounds) {
break
}
if canContinue {
continue
}
currentOutbound := common.Find(s.outbounds, func(it adapter.Outbound) bool {
return !started[outboundTags[it]]
})
var lintOutbound func(oTree []string, oCurrent adapter.Outbound) error
lintOutbound = func(oTree []string, oCurrent adapter.Outbound) error {
problemOutboundTag := common.Find(oCurrent.Dependencies(), func(it string) bool {
return !started[it]
})
if common.Contains(oTree, problemOutboundTag) {
return E.New("circular outbound dependency: ", strings.Join(oTree, " -> "), " -> ", problemOutboundTag)
}
problemOutbound := outbounds[problemOutboundTag]
if problemOutbound == nil {
return E.New("dependency[", problemOutbound, "] not found for outbound[", outboundTags[oCurrent], "]")
}
return lintOutbound(append(oTree, problemOutboundTag), problemOutbound)
}
return lintOutbound([]string{outboundTags[currentOutbound]}, currentOutbound)
}
return nil
}

View File

@@ -1,7 +1,6 @@
package main package main
import ( import (
"go/build"
"os" "os"
"os/exec" "os/exec"
@@ -12,10 +11,6 @@ import (
func main() { func main() {
build_shared.FindSDK() build_shared.FindSDK()
if os.Getenv("build.Default.GOPATH") == "" {
os.Setenv("GOPATH", build.Default.GOPATH)
}
command := exec.Command(os.Args[1], os.Args[2:]...) command := exec.Command(os.Args[1], os.Args[2:]...)
command.Stdout = os.Stdout command.Stdout = os.Stdout
command.Stderr = os.Stderr command.Stderr = os.Stderr

View File

@@ -5,7 +5,6 @@ import (
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"strings"
_ "github.com/sagernet/gomobile/event/key" _ "github.com/sagernet/gomobile/event/key"
"github.com/sagernet/sing-box/cmd/internal/build_shared" "github.com/sagernet/sing-box/cmd/internal/build_shared"
@@ -39,24 +38,18 @@ func main() {
var ( var (
sharedFlags []string sharedFlags []string
debugFlags []string debugFlags []string
sharedTags []string
iosTags []string
debugTags []string
) )
func init() { func init() {
sharedFlags = append(sharedFlags, "-trimpath") sharedFlags = append(sharedFlags, "-trimpath")
sharedFlags = append(sharedFlags, "-ldflags") sharedFlags = append(sharedFlags, "-ldflags")
currentTag, err := build_shared.ReadTag() currentTag, err := build_shared.ReadTag()
if err != nil { if err != nil {
currentTag = "unknown" currentTag = "unknown"
} }
sharedFlags = append(sharedFlags, "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -s -w -buildid=") sharedFlags = append(sharedFlags, "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -s -w -buildid=")
debugFlags = append(debugFlags, "-X github.com/sagernet/sing-box/constant.Version="+currentTag) debugFlags = append(debugFlags, "-X github.com/sagernet/sing-box/constant.Version="+currentTag)
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_clash_api")
iosTags = append(iosTags, "with_dhcp", "with_low_memory", "with_conntrack")
debugTags = append(debugTags, "debug")
} }
func buildAndroid() { func buildAndroid() {
@@ -77,9 +70,9 @@ func buildAndroid() {
args = append(args, "-tags") args = append(args, "-tags")
if !debugEnabled { if !debugEnabled {
args = append(args, strings.Join(sharedTags, ",")) args = append(args, "with_gvisor,with_quic,with_wireguard,with_utls,with_clash_api")
} else { } else {
args = append(args, strings.Join(append(sharedTags, debugTags...), ",")) args = append(args, "with_gvisor,with_quic,with_wireguard,with_utls,with_clash_api,debug")
} }
args = append(args, "./experimental/libbox") args = append(args, "./experimental/libbox")
@@ -107,7 +100,7 @@ func buildiOS() {
args := []string{ args := []string{
"bind", "bind",
"-v", "-v",
"-target", "ios,iossimulator,tvos,tvossimulator,macos", "-target", "ios,iossimulator,macos",
"-libname=box", "-libname=box",
} }
if !debugEnabled { if !debugEnabled {
@@ -116,12 +109,11 @@ func buildiOS() {
args = append(args, debugFlags...) args = append(args, debugFlags...)
} }
tags := append(sharedTags, iosTags...)
args = append(args, "-tags") args = append(args, "-tags")
if !debugEnabled { if !debugEnabled {
args = append(args, strings.Join(tags, ",")) args = append(args, "with_gvisor,with_quic,with_utls,with_clash_api,with_low_memory,with_conntrack")
} else { } else {
args = append(args, strings.Join(append(tags, debugTags...), ",")) args = append(args, "with_gvisor,with_quic,with_utls,with_clash_api,with_low_memory,with_conntrack,debug")
} }
args = append(args, "./experimental/libbox") args = append(args, "./experimental/libbox")
@@ -133,7 +125,7 @@ func buildiOS() {
log.Fatal(err) log.Fatal(err)
} }
copyPath := filepath.Join("..", "sing-box-for-apple") copyPath := filepath.Join("..", "sing-box-for-ios")
if rw.FileExists(copyPath) { if rw.FileExists(copyPath) {
targetDir := filepath.Join(copyPath, "Libbox.xcframework") targetDir := filepath.Join(copyPath, "Libbox.xcframework")
targetDir, _ = filepath.Abs(targetDir) targetDir, _ = filepath.Abs(targetDir)

View File

@@ -9,7 +9,7 @@ import (
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/gofrs/uuid/v5" "github.com/gofrs/uuid"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes" "golang.zx2c4.com/wireguard/wgctrl/wgtypes"
) )

44
cmd/sing-box/debug.go Normal file
View File

@@ -0,0 +1,44 @@
//go:build debug
package main
import (
"encoding/json"
"net/http"
_ "net/http/pprof"
"runtime"
"runtime/debug"
"github.com/sagernet/sing-box/common/badjson"
"github.com/sagernet/sing-box/log"
"github.com/dustin/go-humanize"
)
func init() {
http.HandleFunc("/debug/gc", func(writer http.ResponseWriter, request *http.Request) {
writer.WriteHeader(http.StatusNoContent)
go debug.FreeOSMemory()
})
http.HandleFunc("/debug/memory", func(writer http.ResponseWriter, request *http.Request) {
var memStats runtime.MemStats
runtime.ReadMemStats(&memStats)
var memObject badjson.JSONObject
memObject.Put("heap", humanize.IBytes(memStats.HeapInuse))
memObject.Put("stack", humanize.IBytes(memStats.StackInuse))
memObject.Put("idle", humanize.IBytes(memStats.HeapIdle-memStats.HeapReleased))
memObject.Put("goroutines", runtime.NumGoroutine())
memObject.Put("rss", rusageMaxRSS())
encoder := json.NewEncoder(writer)
encoder.SetIndent("", " ")
encoder.Encode(memObject)
})
go func() {
err := http.ListenAndServe("0.0.0.0:8964", nil)
if err != nil {
log.Debug(err)
}
}()
}

View File

@@ -1,4 +1,6 @@
package box //go:build debug
package main
import ( import (
"runtime" "runtime"

View File

@@ -1,6 +1,6 @@
//go:build !linux //go:build debug && !linux
package box package main
func rusageMaxRSS() float64 { func rusageMaxRSS() float64 {
return -1 return -1

View File

@@ -55,7 +55,7 @@ func WrapQUIC(err error) error {
if err == nil { if err == nil {
return nil return nil
} }
if Contains(err, "canceled by local with error code 0") { if Contains(err, "canceled with error code 0") {
return net.ErrClosed return net.ErrClosed
} }
return err return err

View File

@@ -1,4 +1,4 @@
//go:build go1.20 && !go1.21 //go:build go1.19 && !go1.20
package badtls package badtls
@@ -14,60 +14,39 @@ import (
"sync/atomic" "sync/atomic"
"unsafe" "unsafe"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/bufio" "github.com/sagernet/sing/common/bufio"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
aTLS "github.com/sagernet/sing/common/tls"
) )
type Conn struct { type Conn struct {
*tls.Conn *tls.Conn
writer N.ExtendedWriter writer N.ExtendedWriter
isHandshakeComplete *atomic.Bool activeCall *int32
activeCall *atomic.Int32 closeNotifySent *bool
closeNotifySent *bool version *uint16
version *uint16 rand io.Reader
rand io.Reader halfAccess *sync.Mutex
halfAccess *sync.Mutex halfError *error
halfError *error cipher cipher.AEAD
cipher cipher.AEAD explicitNonceLen int
explicitNonceLen int halfPtr uintptr
halfPtr uintptr halfSeq []byte
halfSeq []byte halfScratchBuf []byte
halfScratchBuf []byte
} }
func TryCreate(conn aTLS.Conn) aTLS.Conn { func Create(conn *tls.Conn) (TLSConn, error) {
tlsConn, ok := conn.(*tls.Conn) if !handshakeComplete(conn) {
if !ok {
return conn
}
badConn, err := Create(tlsConn)
if err != nil {
log.Warn("initialize badtls: ", err)
return conn
}
return badConn
}
func Create(conn *tls.Conn) (aTLS.Conn, error) {
rawConn := reflect.Indirect(reflect.ValueOf(conn))
rawIsHandshakeComplete := rawConn.FieldByName("isHandshakeComplete")
if !rawIsHandshakeComplete.IsValid() || rawIsHandshakeComplete.Kind() != reflect.Struct {
return nil, E.New("badtls: invalid isHandshakeComplete")
}
isHandshakeComplete := (*atomic.Bool)(unsafe.Pointer(rawIsHandshakeComplete.UnsafeAddr()))
if !isHandshakeComplete.Load() {
return nil, E.New("handshake not finished") return nil, E.New("handshake not finished")
} }
rawConn := reflect.Indirect(reflect.ValueOf(conn))
rawActiveCall := rawConn.FieldByName("activeCall") rawActiveCall := rawConn.FieldByName("activeCall")
if !rawActiveCall.IsValid() || rawActiveCall.Kind() != reflect.Struct { if !rawActiveCall.IsValid() || rawActiveCall.Kind() != reflect.Int32 {
return nil, E.New("badtls: invalid active call") return nil, E.New("badtls: invalid active call")
} }
activeCall := (*atomic.Int32)(unsafe.Pointer(rawActiveCall.UnsafeAddr())) activeCall := (*int32)(unsafe.Pointer(rawActiveCall.UnsafeAddr()))
rawHalfConn := rawConn.FieldByName("out") rawHalfConn := rawConn.FieldByName("out")
if !rawHalfConn.IsValid() || rawHalfConn.Kind() != reflect.Struct { if !rawHalfConn.IsValid() || rawHalfConn.Kind() != reflect.Struct {
return nil, E.New("badtls: invalid half conn") return nil, E.New("badtls: invalid half conn")
@@ -129,20 +108,19 @@ func Create(conn *tls.Conn) (aTLS.Conn, error) {
} }
halfScratchBuf := rawHalfScratchBuf.Bytes() halfScratchBuf := rawHalfScratchBuf.Bytes()
return &Conn{ return &Conn{
Conn: conn, Conn: conn,
writer: bufio.NewExtendedWriter(conn.NetConn()), writer: bufio.NewExtendedWriter(conn.NetConn()),
isHandshakeComplete: isHandshakeComplete, activeCall: activeCall,
activeCall: activeCall, closeNotifySent: closeNotifySent,
closeNotifySent: closeNotifySent, version: version,
version: version, halfAccess: halfAccess,
halfAccess: halfAccess, halfError: halfError,
halfError: halfError, cipher: aeadCipher,
cipher: aeadCipher, explicitNonceLen: explicitNonceLen,
explicitNonceLen: explicitNonceLen, rand: randReader,
rand: randReader, halfPtr: rawHalfConn.UnsafeAddr(),
halfPtr: rawHalfConn.UnsafeAddr(), halfSeq: halfSeq,
halfSeq: halfSeq, halfScratchBuf: halfScratchBuf,
halfScratchBuf: halfScratchBuf,
}, nil }, nil
} }
@@ -152,15 +130,15 @@ func (c *Conn) WriteBuffer(buffer *buf.Buffer) error {
return common.Error(c.Write(buffer.Bytes())) return common.Error(c.Write(buffer.Bytes()))
} }
for { for {
x := c.activeCall.Load() x := atomic.LoadInt32(c.activeCall)
if x&1 != 0 { if x&1 != 0 {
return net.ErrClosed return net.ErrClosed
} }
if c.activeCall.CompareAndSwap(x, x+2) { if atomic.CompareAndSwapInt32(c.activeCall, x, x+2) {
break break
} }
} }
defer c.activeCall.Add(-2) defer atomic.AddInt32(c.activeCall, -2)
c.halfAccess.Lock() c.halfAccess.Lock()
defer c.halfAccess.Unlock() defer c.halfAccess.Unlock()
if err := *c.halfError; err != nil { if err := *c.halfError; err != nil {
@@ -208,7 +186,6 @@ func (c *Conn) WriteBuffer(buffer *buf.Buffer) error {
binary.BigEndian.PutUint16(outBuf[3:], uint16(dataLen+c.explicitNonceLen+c.cipher.Overhead())) binary.BigEndian.PutUint16(outBuf[3:], uint16(dataLen+c.explicitNonceLen+c.cipher.Overhead()))
} }
incSeq(c.halfPtr) incSeq(c.halfPtr)
log.Trace("badtls write ", buffer.Len())
return c.writer.WriteBuffer(buffer) return c.writer.WriteBuffer(buffer)
} }

View File

@@ -1,4 +1,4 @@
//go:build !go1.19 || go1.21 //go:build !go1.19 || go1.20
package badtls package badtls

13
common/badtls/conn.go Normal file
View File

@@ -0,0 +1,13 @@
package badtls
import (
"context"
"crypto/tls"
"net"
)
type TLSConn interface {
net.Conn
HandshakeContext(ctx context.Context) error
ConnectionState() tls.ConnectionState
}

View File

@@ -1,8 +1,9 @@
//go:build go1.20 && !go.1.21 //go:build go1.19 && !go.1.20
package badtls package badtls
import ( import (
"crypto/tls"
"reflect" "reflect"
_ "unsafe" _ "unsafe"
) )
@@ -15,6 +16,9 @@ const (
//go:linkname errShutdown crypto/tls.errShutdown //go:linkname errShutdown crypto/tls.errShutdown
var errShutdown error var errShutdown error
//go:linkname handshakeComplete crypto/tls.(*Conn).handshakeComplete
func handshakeComplete(conn *tls.Conn) bool
//go:linkname incSeq crypto/tls.(*halfConn).incSeq //go:linkname incSeq crypto/tls.(*halfConn).incSeq
func incSeq(conn uintptr) func incSeq(conn uintptr)

87
common/debugio/log.go Normal file
View File

@@ -0,0 +1,87 @@
package debugio
import (
"net"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/bufio"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
type LogConn struct {
N.ExtendedConn
logger log.Logger
prefix string
}
func NewLogConn(conn net.Conn, logger log.Logger, prefix string) N.ExtendedConn {
return &LogConn{bufio.NewExtendedConn(conn), logger, prefix}
}
func (c *LogConn) Read(p []byte) (n int, err error) {
n, err = c.ExtendedConn.Read(p)
if n > 0 {
c.logger.Debug(c.prefix, " read ", buf.EncodeHexString(p[:n]))
}
return
}
func (c *LogConn) Write(p []byte) (n int, err error) {
c.logger.Debug(c.prefix, " write ", buf.EncodeHexString(p))
return c.ExtendedConn.Write(p)
}
func (c *LogConn) ReadBuffer(buffer *buf.Buffer) error {
err := c.ExtendedConn.ReadBuffer(buffer)
if err == nil {
c.logger.Debug(c.prefix, " read buffer ", buf.EncodeHexString(buffer.Bytes()))
}
return err
}
func (c *LogConn) WriteBuffer(buffer *buf.Buffer) error {
c.logger.Debug(c.prefix, " write buffer ", buf.EncodeHexString(buffer.Bytes()))
return c.ExtendedConn.WriteBuffer(buffer)
}
func (c *LogConn) Upstream() any {
return c.ExtendedConn
}
type LogPacketConn struct {
N.NetPacketConn
logger log.Logger
prefix string
}
func NewLogPacketConn(conn net.PacketConn, logger log.Logger, prefix string) N.NetPacketConn {
return &LogPacketConn{bufio.NewPacketConn(conn), logger, prefix}
}
func (c *LogPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
n, addr, err = c.NetPacketConn.ReadFrom(p)
if n > 0 {
c.logger.Debug(c.prefix, " read from ", addr, " ", buf.EncodeHexString(p[:n]))
}
return
}
func (c *LogPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
c.logger.Debug(c.prefix, " write to ", addr, " ", buf.EncodeHexString(p))
return c.NetPacketConn.WriteTo(p, addr)
}
func (c *LogPacketConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) {
destination, err = c.NetPacketConn.ReadPacket(buffer)
if err == nil {
c.logger.Debug(c.prefix, " read packet from ", destination, " ", buf.EncodeHexString(buffer.Bytes()))
}
return
}
func (c *LogPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
c.logger.Debug(c.prefix, " write packet to ", destination, " ", buf.EncodeHexString(buffer.Bytes()))
return c.NetPacketConn.WritePacket(buffer, destination)
}

19
common/debugio/print.go Normal file
View File

@@ -0,0 +1,19 @@
package debugio
import (
"fmt"
"reflect"
"github.com/sagernet/sing/common"
)
func PrintUpstream(obj any) {
for obj != nil {
fmt.Println(reflect.TypeOf(obj))
if u, ok := obj.(common.WithUpstream); !ok {
break
} else {
obj = u.Upstream()
}
}
}

48
common/debugio/race.go Normal file
View File

@@ -0,0 +1,48 @@
package debugio
import (
"net"
"sync"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/bufio"
N "github.com/sagernet/sing/common/network"
)
type RaceConn struct {
N.ExtendedConn
readAccess sync.Mutex
writeAccess sync.Mutex
}
func NewRaceConn(conn net.Conn) N.ExtendedConn {
return &RaceConn{ExtendedConn: bufio.NewExtendedConn(conn)}
}
func (c *RaceConn) Read(p []byte) (n int, err error) {
c.readAccess.Lock()
defer c.readAccess.Unlock()
return c.ExtendedConn.Read(p)
}
func (c *RaceConn) Write(p []byte) (n int, err error) {
c.writeAccess.Lock()
defer c.writeAccess.Unlock()
return c.ExtendedConn.Write(p)
}
func (c *RaceConn) ReadBuffer(buffer *buf.Buffer) error {
c.readAccess.Lock()
defer c.readAccess.Unlock()
return c.ExtendedConn.ReadBuffer(buffer)
}
func (c *RaceConn) WriteBuffer(buffer *buf.Buffer) error {
c.writeAccess.Lock()
defer c.writeAccess.Unlock()
return c.ExtendedConn.WriteBuffer(buffer)
}
func (c *RaceConn) Upstream() any {
return c.ExtendedConn
}

View File

@@ -12,7 +12,7 @@ type Conn struct {
element *list.Element[io.Closer] element *list.Element[io.Closer]
} }
func NewConn(conn net.Conn) (net.Conn, error) { func NewConn(conn net.Conn) (*Conn, error) {
connAccess.Lock() connAccess.Lock()
element := openConnection.PushBack(conn) element := openConnection.PushBack(conn)
connAccess.Unlock() connAccess.Unlock()

View File

@@ -4,7 +4,6 @@ import (
"io" "io"
"net" "net"
"github.com/sagernet/sing/common/bufio"
"github.com/sagernet/sing/common/x/list" "github.com/sagernet/sing/common/x/list"
) )
@@ -13,7 +12,7 @@ type PacketConn struct {
element *list.Element[io.Closer] element *list.Element[io.Closer]
} }
func NewPacketConn(conn net.PacketConn) (net.PacketConn, error) { func NewPacketConn(conn net.PacketConn) (*PacketConn, error) {
connAccess.Lock() connAccess.Lock()
element := openConnection.PushBack(conn) element := openConnection.PushBack(conn)
connAccess.Unlock() connAccess.Unlock()
@@ -43,7 +42,7 @@ func (c *PacketConn) Close() error {
} }
func (c *PacketConn) Upstream() any { func (c *PacketConn) Upstream() any {
return bufio.NewPacketConn(c.PacketConn) return c.PacketConn
} }
func (c *PacketConn) ReaderReplaceable() bool { func (c *PacketConn) ReaderReplaceable() bool {

View File

@@ -7,6 +7,7 @@ import (
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/dialer/conntrack" "github.com/sagernet/sing-box/common/dialer/conntrack"
"github.com/sagernet/sing-box/common/warning"
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/control" "github.com/sagernet/sing/common/control"
@@ -16,6 +17,41 @@ import (
"github.com/sagernet/tfo-go" "github.com/sagernet/tfo-go"
) )
var warnBindInterfaceOnUnsupportedPlatform = warning.New(
func() bool {
return !(C.IsLinux || C.IsWindows || C.IsDarwin)
},
"outbound option `bind_interface` is only supported on Linux and Windows",
)
var warnRoutingMarkOnUnsupportedPlatform = warning.New(
func() bool {
return !C.IsLinux
},
"outbound option `routing_mark` is only supported on Linux",
)
var warnReuseAdderOnUnsupportedPlatform = warning.New(
func() bool {
return !(C.IsDarwin || C.IsDragonfly || C.IsFreebsd || C.IsLinux || C.IsNetbsd || C.IsOpenbsd || C.IsSolaris || C.IsWindows)
},
"outbound option `reuse_addr` is unsupported on current platform",
)
var warnProtectPathOnNonAndroid = warning.New(
func() bool {
return !C.IsAndroid
},
"outbound option `protect_path` is only supported on Android",
)
var warnTFOOnUnsupportedPlatform = warning.New(
func() bool {
return !(C.IsDarwin || C.IsFreebsd || C.IsLinux || C.IsWindows)
},
"outbound option `tcp_fast_open` is unsupported on current platform",
)
type DefaultDialer struct { type DefaultDialer struct {
dialer4 tfo.Dialer dialer4 tfo.Dialer
dialer6 tfo.Dialer dialer6 tfo.Dialer
@@ -30,6 +66,7 @@ func NewDefault(router adapter.Router, options option.DialerOptions) *DefaultDia
var dialer net.Dialer var dialer net.Dialer
var listener net.ListenConfig var listener net.ListenConfig
if options.BindInterface != "" { if options.BindInterface != "" {
warnBindInterfaceOnUnsupportedPlatform.Check()
bindFunc := control.BindToInterface(router.InterfaceFinder(), options.BindInterface, -1) bindFunc := control.BindToInterface(router.InterfaceFinder(), options.BindInterface, -1)
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)
@@ -43,6 +80,7 @@ func NewDefault(router adapter.Router, options option.DialerOptions) *DefaultDia
listener.Control = control.Append(listener.Control, bindFunc) listener.Control = control.Append(listener.Control, bindFunc)
} }
if options.RoutingMark != 0 { if options.RoutingMark != 0 {
warnRoutingMarkOnUnsupportedPlatform.Check()
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.DefaultMark() != 0 { } else if router.DefaultMark() != 0 {
@@ -50,9 +88,11 @@ func NewDefault(router adapter.Router, options option.DialerOptions) *DefaultDia
listener.Control = control.Append(listener.Control, control.RoutingMark(router.DefaultMark())) listener.Control = control.Append(listener.Control, control.RoutingMark(router.DefaultMark()))
} }
if options.ReuseAddr { if options.ReuseAddr {
warnReuseAdderOnUnsupportedPlatform.Check()
listener.Control = control.Append(listener.Control, control.ReuseAddr()) listener.Control = control.Append(listener.Control, control.ReuseAddr())
} }
if options.ProtectPath != "" { if options.ProtectPath != "" {
warnProtectPathOnNonAndroid.Check()
dialer.Control = control.Append(dialer.Control, control.ProtectPath(options.ProtectPath)) dialer.Control = control.Append(dialer.Control, control.ProtectPath(options.ProtectPath))
listener.Control = control.Append(listener.Control, control.ProtectPath(options.ProtectPath)) listener.Control = control.Append(listener.Control, control.ProtectPath(options.ProtectPath))
} }
@@ -61,6 +101,9 @@ func NewDefault(router adapter.Router, options option.DialerOptions) *DefaultDia
} else { } else {
dialer.Timeout = C.TCPTimeout dialer.Timeout = C.TCPTimeout
} }
if options.TCPFastOpen {
warnTFOOnUnsupportedPlatform.Check()
}
var udpFragment bool var udpFragment bool
if options.UDPFragment != nil { if options.UDPFragment != nil {
udpFragment = *options.UDPFragment udpFragment = *options.UDPFragment

View File

@@ -73,7 +73,7 @@ func (d *ResolveDialer) ListenPacket(ctx context.Context, destination M.Socksadd
if err != nil { if err != nil {
return nil, err return nil, err
} }
return bufio.NewNATPacketConn(bufio.NewPacketConn(conn), M.SocksaddrFrom(destinationAddress, destination.Port), destination), nil return bufio.NewNATPacketConn(bufio.NewPacketConn(conn), destination, M.SocksaddrFrom(destinationAddress, destination.Port)), nil
} }
func (d *ResolveDialer) Upstream() any { func (d *ResolveDialer) Upstream() any {

View File

@@ -128,6 +128,13 @@ func (c *slowOpenConn) NeedHandshake() bool {
return c.conn == nil return c.conn == nil
} }
func (c *slowOpenConn) ReadFrom(r io.Reader) (n int64, err error) {
if c.conn != nil {
return bufio.Copy(c.conn, r)
}
return bufio.ReadFrom0(c, r)
}
func (c *slowOpenConn) WriteTo(w io.Writer) (n int64, err error) { func (c *slowOpenConn) WriteTo(w io.Writer) (n int64, err error) {
if c.conn == nil { if c.conn == nil {
select { select {

View File

@@ -1,21 +1,520 @@
package mux package mux
import ( import (
"context"
"encoding/binary"
"io"
"net"
"sync"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-mux" "github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/bufio"
"github.com/sagernet/sing/common/bufio/deadline"
E "github.com/sagernet/sing/common/exceptions"
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"
) )
func NewClientWithOptions(dialer N.Dialer, options option.MultiplexOptions) (*Client, error) { var _ N.Dialer = (*Client)(nil)
type Client struct {
access sync.Mutex
connections list.List[abstractSession]
ctx context.Context
dialer N.Dialer
protocol Protocol
maxConnections int
minStreams int
maxStreams int
}
func NewClient(ctx context.Context, dialer N.Dialer, protocol Protocol, maxConnections int, minStreams int, maxStreams int) *Client {
return &Client{
ctx: ctx,
dialer: dialer,
protocol: protocol,
maxConnections: maxConnections,
minStreams: minStreams,
maxStreams: maxStreams,
}
}
func NewClientWithOptions(ctx context.Context, dialer N.Dialer, options option.MultiplexOptions) (N.Dialer, error) {
if !options.Enabled { if !options.Enabled {
return nil, nil return nil, nil
} }
return mux.NewClient(mux.Options{ if options.MaxConnections == 0 && options.MaxStreams == 0 {
Dialer: dialer, options.MinStreams = 8
Protocol: options.Protocol, }
MaxConnections: options.MaxConnections, protocol, err := ParseProtocol(options.Protocol)
MinStreams: options.MinStreams, if err != nil {
MaxStreams: options.MaxStreams, return nil, err
Padding: options.Padding, }
}) return NewClient(ctx, dialer, protocol, options.MaxConnections, options.MinStreams, options.MaxStreams), nil
}
func (c *Client) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
switch N.NetworkName(network) {
case N.NetworkTCP:
stream, err := c.openStream()
if err != nil {
return nil, err
}
return &ClientConn{Conn: stream, destination: destination}, nil
case N.NetworkUDP:
stream, err := c.openStream()
if err != nil {
return nil, err
}
return bufio.NewBindPacketConn(deadline.NewPacketConn(bufio.NewNetPacketConn(&ClientPacketConn{ExtendedConn: bufio.NewExtendedConn(stream), destination: destination})), destination), nil
default:
return nil, E.Extend(N.ErrUnknownNetwork, network)
}
}
func (c *Client) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
stream, err := c.openStream()
if err != nil {
return nil, err
}
return deadline.NewPacketConn(&ClientPacketAddrConn{ExtendedConn: bufio.NewExtendedConn(stream), destination: destination}), nil
}
func (c *Client) openStream() (net.Conn, error) {
var (
session abstractSession
stream net.Conn
err error
)
for attempts := 0; attempts < 2; attempts++ {
session, err = c.offer()
if err != nil {
continue
}
stream, err = session.Open()
if err != nil {
continue
}
break
}
if err != nil {
return nil, err
}
return &wrapStream{stream}, nil
}
func (c *Client) offer() (abstractSession, error) {
c.access.Lock()
defer c.access.Unlock()
sessions := make([]abstractSession, 0, c.maxConnections)
for element := c.connections.Front(); element != nil; {
if element.Value.IsClosed() {
nextElement := element.Next()
c.connections.Remove(element)
element = nextElement
continue
}
sessions = append(sessions, element.Value)
element = element.Next()
}
sLen := len(sessions)
if sLen == 0 {
return c.offerNew()
}
session := common.MinBy(sessions, abstractSession.NumStreams)
numStreams := session.NumStreams()
if numStreams == 0 {
return session, nil
}
if c.maxConnections > 0 {
if sLen >= c.maxConnections || numStreams < c.minStreams {
return session, nil
}
} else {
if c.maxStreams > 0 && numStreams < c.maxStreams {
return session, nil
}
}
return c.offerNew()
}
func (c *Client) offerNew() (abstractSession, error) {
conn, err := c.dialer.DialContext(c.ctx, N.NetworkTCP, Destination)
if err != nil {
return nil, err
}
if vectorisedWriter, isVectorised := bufio.CreateVectorisedWriter(conn); isVectorised {
conn = &vectorisedProtocolConn{protocolConn{Conn: conn, protocol: c.protocol}, vectorisedWriter}
} else {
conn = &protocolConn{Conn: conn, protocol: c.protocol}
}
session, err := c.protocol.newClient(conn)
if err != nil {
return nil, err
}
c.connections.PushBack(session)
return session, nil
}
func (c *Client) Close() error {
c.access.Lock()
defer c.access.Unlock()
for _, session := range c.connections.Array() {
session.Close()
}
return nil
}
type ClientConn struct {
net.Conn
destination M.Socksaddr
requestWrite bool
responseRead bool
}
func (c *ClientConn) readResponse() error {
response, err := ReadStreamResponse(c.Conn)
if err != nil {
return err
}
if response.Status == statusError {
return E.New("remote error: ", response.Message)
}
return nil
}
func (c *ClientConn) Read(b []byte) (n int, err error) {
if !c.responseRead {
err = c.readResponse()
if err != nil {
return
}
c.responseRead = true
}
return c.Conn.Read(b)
}
func (c *ClientConn) Write(b []byte) (n int, err error) {
if c.requestWrite {
return c.Conn.Write(b)
}
request := StreamRequest{
Network: N.NetworkTCP,
Destination: c.destination,
}
_buffer := buf.StackNewSize(requestLen(request) + len(b))
defer common.KeepAlive(_buffer)
buffer := common.Dup(_buffer)
defer buffer.Release()
EncodeStreamRequest(request, buffer)
buffer.Write(b)
_, err = c.Conn.Write(buffer.Bytes())
if err != nil {
return
}
c.requestWrite = true
return len(b), nil
}
func (c *ClientConn) ReadFrom(r io.Reader) (n int64, err error) {
if !c.requestWrite {
return bufio.ReadFrom0(c, r)
}
return bufio.Copy(c.Conn, r)
}
func (c *ClientConn) WriteTo(w io.Writer) (n int64, err error) {
if !c.responseRead {
return bufio.WriteTo0(c, w)
}
return bufio.Copy(w, c.Conn)
}
func (c *ClientConn) LocalAddr() net.Addr {
return c.Conn.LocalAddr()
}
func (c *ClientConn) RemoteAddr() net.Addr {
return c.destination.TCPAddr()
}
func (c *ClientConn) ReaderReplaceable() bool {
return c.responseRead
}
func (c *ClientConn) WriterReplaceable() bool {
return c.requestWrite
}
func (c *ClientConn) Upstream() any {
return c.Conn
}
type ClientPacketConn struct {
N.ExtendedConn
destination M.Socksaddr
requestWrite bool
responseRead bool
}
func (c *ClientPacketConn) readResponse() error {
response, err := ReadStreamResponse(c.ExtendedConn)
if err != nil {
return err
}
if response.Status == statusError {
return E.New("remote error: ", response.Message)
}
return nil
}
func (c *ClientPacketConn) Read(b []byte) (n int, err error) {
if !c.responseRead {
err = c.readResponse()
if err != nil {
return
}
c.responseRead = true
}
var length uint16
err = binary.Read(c.ExtendedConn, binary.BigEndian, &length)
if err != nil {
return
}
if cap(b) < int(length) {
return 0, io.ErrShortBuffer
}
return io.ReadFull(c.ExtendedConn, b[:length])
}
func (c *ClientPacketConn) writeRequest(payload []byte) (n int, err error) {
request := StreamRequest{
Network: N.NetworkUDP,
Destination: c.destination,
}
rLen := requestLen(request)
if len(payload) > 0 {
rLen += 2 + len(payload)
}
_buffer := buf.StackNewSize(rLen)
defer common.KeepAlive(_buffer)
buffer := common.Dup(_buffer)
defer buffer.Release()
EncodeStreamRequest(request, buffer)
if len(payload) > 0 {
common.Must(
binary.Write(buffer, binary.BigEndian, uint16(len(payload))),
common.Error(buffer.Write(payload)),
)
}
_, err = c.ExtendedConn.Write(buffer.Bytes())
if err != nil {
return
}
c.requestWrite = true
return len(payload), nil
}
func (c *ClientPacketConn) Write(b []byte) (n int, err error) {
if !c.requestWrite {
return c.writeRequest(b)
}
err = binary.Write(c.ExtendedConn, binary.BigEndian, uint16(len(b)))
if err != nil {
return
}
return c.ExtendedConn.Write(b)
}
func (c *ClientPacketConn) ReadBuffer(buffer *buf.Buffer) (err error) {
if !c.responseRead {
err = c.readResponse()
if err != nil {
return
}
c.responseRead = true
}
var length uint16
err = binary.Read(c.ExtendedConn, binary.BigEndian, &length)
if err != nil {
return
}
_, err = buffer.ReadFullFrom(c.ExtendedConn, int(length))
return
}
func (c *ClientPacketConn) WriteBuffer(buffer *buf.Buffer) error {
if !c.requestWrite {
defer buffer.Release()
return common.Error(c.writeRequest(buffer.Bytes()))
}
bLen := buffer.Len()
binary.BigEndian.PutUint16(buffer.ExtendHeader(2), uint16(bLen))
return c.ExtendedConn.WriteBuffer(buffer)
}
func (c *ClientPacketConn) FrontHeadroom() int {
return 2
}
func (c *ClientPacketConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) {
err = c.ReadBuffer(buffer)
return
}
func (c *ClientPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
return c.WriteBuffer(buffer)
}
func (c *ClientPacketConn) LocalAddr() net.Addr {
return c.ExtendedConn.LocalAddr()
}
func (c *ClientPacketConn) RemoteAddr() net.Addr {
return c.destination.UDPAddr()
}
func (c *ClientPacketConn) Upstream() any {
return c.ExtendedConn
}
var _ N.NetPacketConn = (*ClientPacketAddrConn)(nil)
type ClientPacketAddrConn struct {
N.ExtendedConn
destination M.Socksaddr
requestWrite bool
responseRead bool
}
func (c *ClientPacketAddrConn) readResponse() error {
response, err := ReadStreamResponse(c.ExtendedConn)
if err != nil {
return err
}
if response.Status == statusError {
return E.New("remote error: ", response.Message)
}
return nil
}
func (c *ClientPacketAddrConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
if !c.responseRead {
err = c.readResponse()
if err != nil {
return
}
c.responseRead = true
}
destination, err := M.SocksaddrSerializer.ReadAddrPort(c.ExtendedConn)
if err != nil {
return
}
addr = destination.UDPAddr()
var length uint16
err = binary.Read(c.ExtendedConn, binary.BigEndian, &length)
if err != nil {
return
}
if cap(p) < int(length) {
return 0, nil, io.ErrShortBuffer
}
n, err = io.ReadFull(c.ExtendedConn, p[:length])
return
}
func (c *ClientPacketAddrConn) writeRequest(payload []byte, destination M.Socksaddr) (n int, err error) {
request := StreamRequest{
Network: N.NetworkUDP,
Destination: c.destination,
PacketAddr: true,
}
rLen := requestLen(request)
if len(payload) > 0 {
rLen += M.SocksaddrSerializer.AddrPortLen(destination) + 2 + len(payload)
}
_buffer := buf.StackNewSize(rLen)
defer common.KeepAlive(_buffer)
buffer := common.Dup(_buffer)
defer buffer.Release()
EncodeStreamRequest(request, buffer)
if len(payload) > 0 {
common.Must(
M.SocksaddrSerializer.WriteAddrPort(buffer, destination),
binary.Write(buffer, binary.BigEndian, uint16(len(payload))),
common.Error(buffer.Write(payload)),
)
}
_, err = c.ExtendedConn.Write(buffer.Bytes())
if err != nil {
return
}
c.requestWrite = true
return len(payload), nil
}
func (c *ClientPacketAddrConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
if !c.requestWrite {
return c.writeRequest(p, M.SocksaddrFromNet(addr))
}
err = M.SocksaddrSerializer.WriteAddrPort(c.ExtendedConn, M.SocksaddrFromNet(addr))
if err != nil {
return
}
err = binary.Write(c.ExtendedConn, binary.BigEndian, uint16(len(p)))
if err != nil {
return
}
return c.ExtendedConn.Write(p)
}
func (c *ClientPacketAddrConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) {
if !c.responseRead {
err = c.readResponse()
if err != nil {
return
}
c.responseRead = true
}
destination, err = M.SocksaddrSerializer.ReadAddrPort(c.ExtendedConn)
if err != nil {
return
}
var length uint16
err = binary.Read(c.ExtendedConn, binary.BigEndian, &length)
if err != nil {
return
}
_, err = buffer.ReadFullFrom(c.ExtendedConn, int(length))
return
}
func (c *ClientPacketAddrConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
if !c.requestWrite {
defer buffer.Release()
return common.Error(c.writeRequest(buffer.Bytes(), destination))
}
bLen := buffer.Len()
header := buf.With(buffer.ExtendHeader(M.SocksaddrSerializer.AddrPortLen(destination) + 2))
common.Must(
M.SocksaddrSerializer.WriteAddrPort(header, destination),
binary.Write(header, binary.BigEndian, uint16(bLen)),
)
return c.ExtendedConn.WriteBuffer(buffer)
}
func (c *ClientPacketAddrConn) LocalAddr() net.Addr {
return c.ExtendedConn.LocalAddr()
}
func (c *ClientPacketAddrConn) FrontHeadroom() int {
return 2 + M.MaxSocksaddrLength
}
func (c *ClientPacketAddrConn) Upstream() any {
return c.ExtendedConn
} }

View File

@@ -1,14 +1,240 @@
package mux package mux
import ( import (
"github.com/sagernet/sing-mux" "encoding/binary"
"io"
"net"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/buf"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/rw"
"github.com/sagernet/smux"
"github.com/hashicorp/yamux"
) )
type ( var Destination = M.Socksaddr{
Client = mux.Client Fqdn: "sp.mux.sing-box.arpa",
Port: 444,
}
const (
ProtocolSMux Protocol = iota
ProtocolYAMux
) )
var ( type Protocol byte
Destination = mux.Destination
HandleConnection = mux.HandleConnection func ParseProtocol(name string) (Protocol, error) {
switch name {
case "", "smux":
return ProtocolSMux, nil
case "yamux":
return ProtocolYAMux, nil
default:
return ProtocolYAMux, E.New("unknown multiplex protocol: ", name)
}
}
func (p Protocol) newServer(conn net.Conn) (abstractSession, error) {
switch p {
case ProtocolSMux:
session, err := smux.Server(conn, smuxConfig())
if err != nil {
return nil, err
}
return &smuxSession{session}, nil
case ProtocolYAMux:
return yamux.Server(conn, yaMuxConfig())
default:
panic("unknown protocol")
}
}
func (p Protocol) newClient(conn net.Conn) (abstractSession, error) {
switch p {
case ProtocolSMux:
session, err := smux.Client(conn, smuxConfig())
if err != nil {
return nil, err
}
return &smuxSession{session}, nil
case ProtocolYAMux:
return yamux.Client(conn, yaMuxConfig())
default:
panic("unknown protocol")
}
}
func smuxConfig() *smux.Config {
config := smux.DefaultConfig()
config.KeepAliveDisabled = true
return config
}
func yaMuxConfig() *yamux.Config {
config := yamux.DefaultConfig()
config.LogOutput = io.Discard
config.StreamCloseTimeout = C.TCPTimeout
config.StreamOpenTimeout = C.TCPTimeout
return config
}
func (p Protocol) String() string {
switch p {
case ProtocolSMux:
return "smux"
case ProtocolYAMux:
return "yamux"
default:
return "unknown"
}
}
const (
version0 = 0
) )
type Request struct {
Protocol Protocol
}
func ReadRequest(reader io.Reader) (*Request, error) {
version, err := rw.ReadByte(reader)
if err != nil {
return nil, err
}
if version != version0 {
return nil, E.New("unsupported version: ", version)
}
protocol, err := rw.ReadByte(reader)
if err != nil {
return nil, err
}
if protocol > byte(ProtocolYAMux) {
return nil, E.New("unsupported protocol: ", protocol)
}
return &Request{Protocol: Protocol(protocol)}, nil
}
func EncodeRequest(buffer *buf.Buffer, request Request) {
buffer.WriteByte(version0)
buffer.WriteByte(byte(request.Protocol))
}
const (
flagUDP = 1
flagAddr = 2
statusSuccess = 0
statusError = 1
)
type StreamRequest struct {
Network string
Destination M.Socksaddr
PacketAddr bool
}
func ReadStreamRequest(reader io.Reader) (*StreamRequest, error) {
var flags uint16
err := binary.Read(reader, binary.BigEndian, &flags)
if err != nil {
return nil, err
}
destination, err := M.SocksaddrSerializer.ReadAddrPort(reader)
if err != nil {
return nil, err
}
var network string
var udpAddr bool
if flags&flagUDP == 0 {
network = N.NetworkTCP
} else {
network = N.NetworkUDP
udpAddr = flags&flagAddr != 0
}
return &StreamRequest{network, destination, udpAddr}, nil
}
func requestLen(request StreamRequest) int {
var rLen int
rLen += 1 // version
rLen += 2 // flags
rLen += M.SocksaddrSerializer.AddrPortLen(request.Destination)
return rLen
}
func EncodeStreamRequest(request StreamRequest, buffer *buf.Buffer) {
destination := request.Destination
var flags uint16
if request.Network == N.NetworkUDP {
flags |= flagUDP
}
if request.PacketAddr {
flags |= flagAddr
if !destination.IsValid() {
destination = Destination
}
}
common.Must(
binary.Write(buffer, binary.BigEndian, flags),
M.SocksaddrSerializer.WriteAddrPort(buffer, destination),
)
}
type StreamResponse struct {
Status uint8
Message string
}
func ReadStreamResponse(reader io.Reader) (*StreamResponse, error) {
var response StreamResponse
status, err := rw.ReadByte(reader)
if err != nil {
return nil, err
}
response.Status = status
if status == statusError {
response.Message, err = rw.ReadVString(reader)
if err != nil {
return nil, err
}
}
return &response, nil
}
type wrapStream struct {
net.Conn
}
func (w *wrapStream) Read(p []byte) (n int, err error) {
n, err = w.Conn.Read(p)
err = wrapError(err)
return
}
func (w *wrapStream) Write(p []byte) (n int, err error) {
n, err = w.Conn.Write(p)
err = wrapError(err)
return
}
func (w *wrapStream) WriteIsThreadUnsafe() {
}
func (w *wrapStream) Upstream() any {
return w.Conn
}
func wrapError(err error) error {
switch err {
case yamux.ErrStreamClosed:
return io.EOF
default:
return err
}
}

258
common/mux/service.go Normal file
View File

@@ -0,0 +1,258 @@
package mux
import (
"context"
"encoding/binary"
"net"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/bufio"
"github.com/sagernet/sing/common/bufio/deadline"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/rw"
"github.com/sagernet/sing/common/task"
)
func NewConnection(ctx context.Context, router adapter.Router, errorHandler E.Handler, logger log.ContextLogger, conn net.Conn, metadata adapter.InboundContext) error {
request, err := ReadRequest(conn)
if err != nil {
return err
}
session, err := request.Protocol.newServer(conn)
if err != nil {
return err
}
var group task.Group
group.Append0(func(ctx context.Context) error {
var stream net.Conn
for {
stream, err = session.Accept()
if err != nil {
return err
}
go newConnection(ctx, router, errorHandler, logger, stream, metadata)
}
})
group.Cleanup(func() {
session.Close()
})
return group.Run(ctx)
}
func newConnection(ctx context.Context, router adapter.Router, errorHandler E.Handler, logger log.ContextLogger, stream net.Conn, metadata adapter.InboundContext) {
stream = &wrapStream{stream}
request, err := ReadStreamRequest(stream)
if err != nil {
logger.ErrorContext(ctx, err)
return
}
metadata.Destination = request.Destination
if request.Network == N.NetworkTCP {
logger.InfoContext(ctx, "inbound multiplex connection to ", metadata.Destination)
hErr := router.RouteConnection(ctx, &ServerConn{ExtendedConn: bufio.NewExtendedConn(stream)}, metadata)
stream.Close()
if hErr != nil {
errorHandler.NewError(ctx, hErr)
}
} else {
var packetConn N.PacketConn
if !request.PacketAddr {
logger.InfoContext(ctx, "inbound multiplex packet connection to ", metadata.Destination)
packetConn = &ServerPacketConn{ExtendedConn: bufio.NewExtendedConn(stream), destination: request.Destination}
} else {
logger.InfoContext(ctx, "inbound multiplex packet connection")
packetConn = &ServerPacketAddrConn{ExtendedConn: bufio.NewExtendedConn(stream)}
}
hErr := router.RoutePacketConnection(ctx, deadline.NewPacketConn(bufio.NewNetPacketConn(packetConn)), metadata)
stream.Close()
if hErr != nil {
errorHandler.NewError(ctx, hErr)
}
}
}
var _ N.HandshakeConn = (*ServerConn)(nil)
type ServerConn struct {
N.ExtendedConn
responseWrite bool
}
func (c *ServerConn) HandshakeFailure(err error) error {
errMessage := err.Error()
_buffer := buf.StackNewSize(1 + rw.UVariantLen(uint64(len(errMessage))) + len(errMessage))
defer common.KeepAlive(_buffer)
buffer := common.Dup(_buffer)
defer buffer.Release()
common.Must(
buffer.WriteByte(statusError),
rw.WriteVString(_buffer, errMessage),
)
return c.ExtendedConn.WriteBuffer(buffer)
}
func (c *ServerConn) Write(b []byte) (n int, err error) {
if c.responseWrite {
return c.ExtendedConn.Write(b)
}
_buffer := buf.StackNewSize(1 + len(b))
defer common.KeepAlive(_buffer)
buffer := common.Dup(_buffer)
defer buffer.Release()
common.Must(
buffer.WriteByte(statusSuccess),
common.Error(buffer.Write(b)),
)
_, err = c.ExtendedConn.Write(buffer.Bytes())
if err != nil {
return
}
c.responseWrite = true
return len(b), nil
}
func (c *ServerConn) WriteBuffer(buffer *buf.Buffer) error {
if c.responseWrite {
return c.ExtendedConn.WriteBuffer(buffer)
}
buffer.ExtendHeader(1)[0] = statusSuccess
c.responseWrite = true
return c.ExtendedConn.WriteBuffer(buffer)
}
func (c *ServerConn) FrontHeadroom() int {
if !c.responseWrite {
return 1
}
return 0
}
func (c *ServerConn) Upstream() any {
return c.ExtendedConn
}
var (
_ N.HandshakeConn = (*ServerPacketConn)(nil)
_ N.PacketConn = (*ServerPacketConn)(nil)
)
type ServerPacketConn struct {
N.ExtendedConn
destination M.Socksaddr
responseWrite bool
}
func (c *ServerPacketConn) HandshakeFailure(err error) error {
errMessage := err.Error()
_buffer := buf.StackNewSize(1 + rw.UVariantLen(uint64(len(errMessage))) + len(errMessage))
defer common.KeepAlive(_buffer)
buffer := common.Dup(_buffer)
defer buffer.Release()
common.Must(
buffer.WriteByte(statusError),
rw.WriteVString(_buffer, errMessage),
)
return c.ExtendedConn.WriteBuffer(buffer)
}
func (c *ServerPacketConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) {
var length uint16
err = binary.Read(c.ExtendedConn, binary.BigEndian, &length)
if err != nil {
return
}
_, err = buffer.ReadFullFrom(c.ExtendedConn, int(length))
if err != nil {
return
}
destination = c.destination
return
}
func (c *ServerPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
pLen := buffer.Len()
common.Must(binary.Write(buf.With(buffer.ExtendHeader(2)), binary.BigEndian, uint16(pLen)))
if !c.responseWrite {
buffer.ExtendHeader(1)[0] = statusSuccess
c.responseWrite = true
}
return c.ExtendedConn.WriteBuffer(buffer)
}
func (c *ServerPacketConn) Upstream() any {
return c.ExtendedConn
}
func (c *ServerPacketConn) FrontHeadroom() int {
if !c.responseWrite {
return 3
}
return 2
}
var (
_ N.HandshakeConn = (*ServerPacketAddrConn)(nil)
_ N.PacketConn = (*ServerPacketAddrConn)(nil)
)
type ServerPacketAddrConn struct {
N.ExtendedConn
responseWrite bool
}
func (c *ServerPacketAddrConn) HandshakeFailure(err error) error {
errMessage := err.Error()
_buffer := buf.StackNewSize(1 + rw.UVariantLen(uint64(len(errMessage))) + len(errMessage))
defer common.KeepAlive(_buffer)
buffer := common.Dup(_buffer)
defer buffer.Release()
common.Must(
buffer.WriteByte(statusError),
rw.WriteVString(_buffer, errMessage),
)
return c.ExtendedConn.WriteBuffer(buffer)
}
func (c *ServerPacketAddrConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) {
destination, err = M.SocksaddrSerializer.ReadAddrPort(c.ExtendedConn)
if err != nil {
return
}
var length uint16
err = binary.Read(c.ExtendedConn, binary.BigEndian, &length)
if err != nil {
return
}
_, err = buffer.ReadFullFrom(c.ExtendedConn, int(length))
if err != nil {
return
}
return
}
func (c *ServerPacketAddrConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
pLen := buffer.Len()
common.Must(binary.Write(buf.With(buffer.ExtendHeader(2)), binary.BigEndian, uint16(pLen)))
common.Must(M.SocksaddrSerializer.WriteAddrPort(buf.With(buffer.ExtendHeader(M.SocksaddrSerializer.AddrPortLen(destination))), destination))
if !c.responseWrite {
buffer.ExtendHeader(1)[0] = statusSuccess
c.responseWrite = true
}
return c.ExtendedConn.WriteBuffer(buffer)
}
func (c *ServerPacketAddrConn) Upstream() any {
return c.ExtendedConn
}
func (c *ServerPacketAddrConn) FrontHeadroom() int {
if !c.responseWrite {
return 3 + M.MaxSocksaddrLength
}
return 2 + M.MaxSocksaddrLength
}

91
common/mux/session.go Normal file
View File

@@ -0,0 +1,91 @@
package mux
import (
"io"
"net"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/bufio"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/smux"
)
type abstractSession interface {
Open() (net.Conn, error)
Accept() (net.Conn, error)
NumStreams() int
Close() error
IsClosed() bool
}
var _ abstractSession = (*smuxSession)(nil)
type smuxSession struct {
*smux.Session
}
func (s *smuxSession) Open() (net.Conn, error) {
return s.OpenStream()
}
func (s *smuxSession) Accept() (net.Conn, error) {
return s.AcceptStream()
}
type protocolConn struct {
net.Conn
protocol Protocol
protocolWritten bool
}
func (c *protocolConn) Write(p []byte) (n int, err error) {
if c.protocolWritten {
return c.Conn.Write(p)
}
_buffer := buf.StackNewSize(2 + len(p))
defer common.KeepAlive(_buffer)
buffer := common.Dup(_buffer)
defer buffer.Release()
EncodeRequest(buffer, Request{
Protocol: c.protocol,
})
common.Must(common.Error(buffer.Write(p)))
n, err = c.Conn.Write(buffer.Bytes())
if err == nil {
n--
}
c.protocolWritten = true
return n, err
}
func (c *protocolConn) ReadFrom(r io.Reader) (n int64, err error) {
if !c.protocolWritten {
return bufio.ReadFrom0(c, r)
}
return bufio.Copy(c.Conn, r)
}
func (c *protocolConn) Upstream() any {
return c.Conn
}
type vectorisedProtocolConn struct {
protocolConn
N.VectorisedWriter
}
func (c *vectorisedProtocolConn) WriteVectorised(buffers []*buf.Buffer) error {
if c.protocolWritten {
return c.VectorisedWriter.WriteVectorised(buffers)
}
c.protocolWritten = true
_buffer := buf.StackNewSize(2)
defer common.KeepAlive(_buffer)
buffer := common.Dup(_buffer)
defer buffer.Release()
EncodeRequest(buffer, Request{
Protocol: c.protocol,
})
return c.VectorisedWriter.WriteVectorised(append([]*buf.Buffer{buffer}, buffers...))
}

View File

@@ -3,12 +3,10 @@ package process
import ( import (
"context" "context"
"net/netip" "net/netip"
"os/user"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-tun" "github.com/sagernet/sing-tun"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
) )
type Searcher interface { type Searcher interface {
@@ -30,15 +28,5 @@ type Info struct {
} }
func FindProcessInfo(searcher Searcher, ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*Info, error) { func FindProcessInfo(searcher Searcher, ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*Info, error) {
info, err := searcher.FindProcessInfo(ctx, network, source, destination) return findProcessInfo(searcher, ctx, network, source, destination)
if err != nil {
return nil, err
}
if info.UserId != -1 {
osUser, _ := user.LookupId(F.ToString(info.UserId))
if osUser != nil {
info.User = osUser.Username
}
}
return info, nil
} }

View File

@@ -15,6 +15,7 @@ import (
"unicode" "unicode"
"unsafe" "unsafe"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/buf"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
@@ -81,7 +82,9 @@ func resolveSocketByNetlink(network string, source netip.AddrPort, destination n
return 0, 0, E.Cause(err, "write netlink request") return 0, 0, E.Cause(err, "write netlink request")
} }
buffer := buf.New() _buffer := buf.StackNew()
defer common.KeepAlive(_buffer)
buffer := common.Dup(_buffer)
defer buffer.Release() defer buffer.Release()
n, err := syscall.Read(socket, buffer.FreeBytes()) n, err := syscall.Read(socket, buffer.FreeBytes())

View File

@@ -0,0 +1,25 @@
//go:build linux && !android
package process
import (
"context"
"net/netip"
"os/user"
F "github.com/sagernet/sing/common/format"
)
func findProcessInfo(searcher Searcher, ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*Info, error) {
info, err := searcher.FindProcessInfo(ctx, network, source, destination)
if err != nil {
return nil, err
}
if info.UserId != -1 {
osUser, _ := user.LookupId(F.ToString(info.UserId))
if osUser != nil {
info.User = osUser.Username
}
}
return info, nil
}

View File

@@ -0,0 +1,12 @@
//go:build !linux || android
package process
import (
"context"
"net/netip"
)
func findProcessInfo(searcher Searcher, ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*Info, error) {
return searcher.FindProcessInfo(ctx, network, source, destination)
}

View File

@@ -26,7 +26,9 @@ func StreamDomainNameQuery(readCtx context.Context, reader io.Reader) (*adapter.
if length == 0 { if length == 0 {
return nil, os.ErrInvalid return nil, os.ErrInvalid
} }
buffer := buf.NewSize(int(length)) _buffer := buf.StackNewSize(int(length))
defer common.KeepAlive(_buffer)
buffer := common.Dup(_buffer)
defer buffer.Release() defer buffer.Release()
readCtx, cancel := context.WithTimeout(readCtx, time.Millisecond*100) readCtx, cancel := context.WithTimeout(readCtx, time.Millisecond*100)

View File

@@ -7,7 +7,6 @@ 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"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/protocol/http" "github.com/sagernet/sing/protocol/http"
) )
@@ -16,5 +15,5 @@ func HTTPHost(ctx context.Context, reader io.Reader) (*adapter.InboundContext, e
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &adapter.InboundContext{Protocol: C.ProtocolHTTP, Domain: M.ParseSocksaddr(request.Host).AddrString()}, nil return &adapter.InboundContext{Protocol: C.ProtocolHTTP, Domain: request.Host}, nil
} }

View File

@@ -1,27 +0,0 @@
package sniff_test
import (
"context"
"strings"
"testing"
"github.com/sagernet/sing-box/common/sniff"
"github.com/stretchr/testify/require"
)
func TestSniffHTTP1(t *testing.T) {
t.Parallel()
pkt := "GET / HTTP/1.1\r\nHost: www.google.com\r\nAccept: */*\r\n\r\n"
metadata, err := sniff.HTTPHost(context.Background(), strings.NewReader(pkt))
require.NoError(t, err)
require.Equal(t, metadata.Domain, "www.google.com")
}
func TestSniffHTTP1WithPort(t *testing.T) {
t.Parallel()
pkt := "GET / HTTP/1.1\r\nHost: www.gov.cn:8080\r\nAccept: */*\r\n\r\n"
metadata, err := sniff.HTTPHost(context.Background(), strings.NewReader(pkt))
require.NoError(t, err)
require.Equal(t, metadata.Domain, "www.gov.cn")
}

View File

@@ -24,12 +24,12 @@ func PeekStream(ctx context.Context, conn net.Conn, buffer *buf.Buffer, timeout
} }
err := conn.SetReadDeadline(time.Now().Add(timeout)) err := conn.SetReadDeadline(time.Now().Add(timeout))
if err != nil { if err != nil {
return nil, E.Cause(err, "set read deadline") return nil, err
} }
_, err = buffer.ReadOnceFrom(conn) _, err = buffer.ReadOnceFrom(conn)
err = E.Errors(err, conn.SetReadDeadline(time.Time{})) err = E.Errors(err, conn.SetReadDeadline(time.Time{}))
if err != nil { if err != nil {
return nil, E.Cause(err, "read payload") return nil, err
} }
var metadata *adapter.InboundContext var metadata *adapter.InboundContext
var errors []error var errors []error

View File

@@ -21,7 +21,6 @@ import (
type acmeWrapper struct { type acmeWrapper struct {
ctx context.Context ctx context.Context
cfg *certmagic.Config cfg *certmagic.Config
cache *certmagic.Cache
domain []string domain []string
} }
@@ -30,7 +29,7 @@ func (w *acmeWrapper) Start() error {
} }
func (w *acmeWrapper) Close() error { func (w *acmeWrapper) Close() error {
w.cache.Stop() w.cfg.Unmanage(w.domain)
return nil return nil
} }
@@ -78,11 +77,10 @@ func startACME(ctx context.Context, options option.InboundACMEOptions) (*tls.Con
acmeConfig.ExternalAccount = (*acme.EAB)(options.ExternalAccount) acmeConfig.ExternalAccount = (*acme.EAB)(options.ExternalAccount)
} }
config.Issuers = []certmagic.Issuer{certmagic.NewACMEIssuer(config, acmeConfig)} config.Issuers = []certmagic.Issuer{certmagic.NewACMEIssuer(config, acmeConfig)}
cache := certmagic.NewCache(certmagic.CacheOptions{ config = certmagic.New(certmagic.NewCache(certmagic.CacheOptions{
GetConfigForCert: func(certificate certmagic.Certificate) (*certmagic.Config, error) { GetConfigForCert: func(certificate certmagic.Certificate) (*certmagic.Config, error) {
return config, nil return config, nil
}, },
}) }), *config)
config = certmagic.New(cache, *config) return config.TLSConfig(), &acmeWrapper{ctx, config, options.Domain}, nil
return config.TLSConfig(), &acmeWrapper{ctx: ctx, cfg: config, cache: cache, domain: options.Domain}, nil
} }

View File

@@ -111,16 +111,6 @@ func (e *RealityClientConfig) ClientHandshake(ctx context.Context, conn net.Conn
if err != nil { if err != nil {
return nil, err return nil, err
} }
if len(uConfig.NextProtos) > 0 {
for _, extension := range uConn.Extensions {
if alpnExtension, isALPN := extension.(*utls.ALPNExtension); isALPN {
alpnExtension.AlpnProtocols = uConfig.NextProtos
break
}
}
}
hello := uConn.HandshakeState.Hello hello := uConn.HandshakeState.Hello
hello.SessionId = make([]byte, 32) hello.SessionId = make([]byte, 32)
copy(hello.Raw[39:], hello.SessionId) copy(hello.Raw[39:], hello.SessionId)
@@ -135,7 +125,7 @@ func (e *RealityClientConfig) ClientHandshake(ctx context.Context, conn net.Conn
hello.SessionId[0] = 1 hello.SessionId[0] = 1
hello.SessionId[1] = 8 hello.SessionId[1] = 8
hello.SessionId[2] = 1 hello.SessionId[2] = 0
binary.BigEndian.PutUint32(hello.SessionId[4:], uint32(time.Now().Unix())) binary.BigEndian.PutUint32(hello.SessionId[4:], uint32(time.Now().Unix()))
copy(hello.SessionId[8:], e.shortID[:]) copy(hello.SessionId[8:], e.shortID[:])

View File

@@ -180,7 +180,7 @@ func NewSTDServer(ctx context.Context, router adapter.Router, logger log.Logger,
tlsConfig.ServerName = options.ServerName tlsConfig.ServerName = options.ServerName
} }
if len(options.ALPN) > 0 { if len(options.ALPN) > 0 {
tlsConfig.NextProtos = append(options.ALPN, tlsConfig.NextProtos...) tlsConfig.NextProtos = append(tlsConfig.NextProtos, options.ALPN...)
} }
if options.MinVersion != "" { if options.MinVersion != "" {
minVersion, err := ParseTLSVersion(options.MinVersion) minVersion, err := ParseTLSVersion(options.MinVersion)

View File

@@ -3,7 +3,6 @@
package tls package tls
import ( import (
"context"
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"math/rand" "math/rand"
@@ -48,7 +47,7 @@ func (e *UTLSClientConfig) Config() (*STDConfig, error) {
} }
func (e *UTLSClientConfig) Client(conn net.Conn) (Conn, error) { func (e *UTLSClientConfig) Client(conn net.Conn) (Conn, error) {
return &utlsALPNWrapper{utlsConnWrapper{utls.UClient(conn, e.config.Clone(), e.id)}, e.config.NextProtos}, nil return &utlsConnWrapper{utls.UClient(conn, e.config.Clone(), e.id)}, nil
} }
func (e *UTLSClientConfig) SetSessionIDGenerator(generator func(clientHello []byte, sessionID []byte) error) { func (e *UTLSClientConfig) SetSessionIDGenerator(generator func(clientHello []byte, sessionID []byte) error) {
@@ -88,31 +87,6 @@ func (c *utlsConnWrapper) Upstream() any {
return c.UConn return c.UConn
} }
type utlsALPNWrapper struct {
utlsConnWrapper
nextProtocols []string
}
func (c *utlsALPNWrapper) HandshakeContext(ctx context.Context) error {
if len(c.nextProtocols) > 0 {
err := c.BuildHandshakeState()
if err != nil {
return err
}
for _, extension := range c.Extensions {
if alpnExtension, isALPN := extension.(*utls.ALPNExtension); isALPN {
alpnExtension.AlpnProtocols = c.nextProtocols
err = c.BuildHandshakeState()
if err != nil {
return err
}
break
}
}
}
return c.UConn.HandshakeContext(ctx)
}
func NewUTLSClient(router adapter.Router, serverAddress string, options option.OutboundTLSOptions) (*UTLSClientConfig, error) { func NewUTLSClient(router adapter.Router, serverAddress string, options option.OutboundTLSOptions) (*UTLSClientConfig, error) {
var serverName string var serverName string
if options.ServerName != "" { if options.ServerName != "" {

View File

@@ -10,7 +10,6 @@ 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/x/list"
) )
type History struct { type History struct {
@@ -21,7 +20,6 @@ type History struct {
type HistoryStorage struct { type HistoryStorage struct {
access sync.RWMutex access sync.RWMutex
delayHistory map[string]*History delayHistory map[string]*History
callbacks list.List[func()]
} }
func NewHistoryStorage() *HistoryStorage { func NewHistoryStorage() *HistoryStorage {
@@ -30,18 +28,6 @@ func NewHistoryStorage() *HistoryStorage {
} }
} }
func (s *HistoryStorage) AddListener(listener func()) *list.Element[func()] {
s.access.Lock()
defer s.access.Unlock()
return s.callbacks.PushBack(listener)
}
func (s *HistoryStorage) RemoveListener(element *list.Element[func()]) {
s.access.Lock()
defer s.access.Unlock()
s.callbacks.Remove(element)
}
func (s *HistoryStorage) LoadURLTestHistory(tag string) *History { func (s *HistoryStorage) LoadURLTestHistory(tag string) *History {
if s == nil { if s == nil {
return nil return nil
@@ -53,30 +39,17 @@ func (s *HistoryStorage) LoadURLTestHistory(tag string) *History {
func (s *HistoryStorage) DeleteURLTestHistory(tag string) { func (s *HistoryStorage) DeleteURLTestHistory(tag string) {
s.access.Lock() s.access.Lock()
defer s.access.Unlock()
delete(s.delayHistory, tag) delete(s.delayHistory, tag)
s.access.Unlock()
s.notifyUpdated()
} }
func (s *HistoryStorage) StoreURLTestHistory(tag string, history *History) { func (s *HistoryStorage) StoreURLTestHistory(tag string, history *History) {
s.access.Lock() s.access.Lock()
defer s.access.Unlock()
s.delayHistory[tag] = history s.delayHistory[tag] = history
s.access.Unlock()
s.notifyUpdated()
}
func (s *HistoryStorage) notifyUpdated() {
s.access.RLock()
defer s.access.RUnlock()
for element := s.callbacks.Front(); element != nil; element = element.Next() {
element.Value()
}
} }
func URLTest(ctx context.Context, link string, detour N.Dialer) (t uint16, err error) { func URLTest(ctx context.Context, link string, detour N.Dialer) (t uint16, err error) {
if link == "" {
link = "https://www.gstatic.com/generate_204"
}
linkURL, err := url.Parse(link) linkURL, err := url.Parse(link)
if err != nil { if err != nil {
return return

31
common/warning/warning.go Normal file
View File

@@ -0,0 +1,31 @@
package warning
import (
"sync"
"github.com/sagernet/sing-box/log"
)
type Warning struct {
logger log.Logger
check CheckFunc
message string
checkOnce sync.Once
}
type CheckFunc = func() bool
func New(checkFunc CheckFunc, message string) Warning {
return Warning{
check: checkFunc,
message: message,
}
}
func (w *Warning) Check() {
w.checkOnce.Do(func() {
if w.check() {
log.Warn(w.message)
}
})
}

View File

@@ -3,13 +3,40 @@ package constant
import ( import (
"os" "os"
"path/filepath" "path/filepath"
"strings"
"github.com/sagernet/sing/common/rw" "github.com/sagernet/sing/common/rw"
) )
const dirName = "sing-box" const dirName = "sing-box"
var resourcePaths []string var (
basePath string
tempPath string
resourcePaths []string
)
func BasePath(name string) string {
if basePath == "" || strings.HasPrefix(name, "/") {
return name
}
return filepath.Join(basePath, name)
}
func CreateTemp(pattern string) (*os.File, error) {
if tempPath == "" {
tempPath = os.TempDir()
}
return os.CreateTemp(tempPath, pattern)
}
func SetBasePath(path string) {
basePath = path
}
func SetTempPath(path string) {
tempPath = path
}
func FindPath(name string) (string, bool) { func FindPath(name string) (string, bool) {
name = os.ExpandEnv(name) name = os.ExpandEnv(name)

View File

@@ -10,7 +10,6 @@ import (
) )
func applyDebugOptions(options option.DebugOptions) { func applyDebugOptions(options option.DebugOptions) {
applyDebugListenOption(options)
if options.GCPercent != nil { if options.GCPercent != nil {
debug.SetGCPercent(*options.GCPercent) debug.SetGCPercent(*options.GCPercent)
} }

View File

@@ -10,7 +10,6 @@ import (
) )
func applyDebugOptions(options option.DebugOptions) { func applyDebugOptions(options option.DebugOptions) {
applyDebugListenOption(options)
if options.GCPercent != nil { if options.GCPercent != nil {
debug.SetGCPercent(*options.GCPercent) debug.SetGCPercent(*options.GCPercent)
} }

View File

@@ -1,67 +0,0 @@
package box
import (
"net/http"
"net/http/pprof"
"runtime"
"runtime/debug"
"github.com/sagernet/sing-box/common/badjson"
"github.com/sagernet/sing-box/common/json"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/dustin/go-humanize"
"github.com/go-chi/chi/v5"
)
var debugHTTPServer *http.Server
func applyDebugListenOption(options option.DebugOptions) {
if debugHTTPServer != nil {
debugHTTPServer.Close()
debugHTTPServer = nil
}
if options.Listen == "" {
return
}
r := chi.NewMux()
r.Route("/debug", func(r chi.Router) {
r.Get("/gc", func(writer http.ResponseWriter, request *http.Request) {
writer.WriteHeader(http.StatusNoContent)
go debug.FreeOSMemory()
})
r.Get("/memory", func(writer http.ResponseWriter, request *http.Request) {
var memStats runtime.MemStats
runtime.ReadMemStats(&memStats)
var memObject badjson.JSONObject
memObject.Put("heap", humanize.IBytes(memStats.HeapInuse))
memObject.Put("stack", humanize.IBytes(memStats.StackInuse))
memObject.Put("idle", humanize.IBytes(memStats.HeapIdle-memStats.HeapReleased))
memObject.Put("goroutines", runtime.NumGoroutine())
memObject.Put("rss", rusageMaxRSS())
encoder := json.NewEncoder(writer)
encoder.SetIndent("", " ")
encoder.Encode(memObject)
})
r.HandleFunc("/pprof", pprof.Index)
r.HandleFunc("/pprof/*", pprof.Index)
r.HandleFunc("/pprof/cmdline", pprof.Cmdline)
r.HandleFunc("/pprof/profile", pprof.Profile)
r.HandleFunc("/pprof/symbol", pprof.Symbol)
r.HandleFunc("/pprof/trace", pprof.Trace)
})
debugHTTPServer = &http.Server{
Addr: options.Listen,
Handler: r,
}
go func() {
err := debugHTTPServer.ListenAndServe()
if err != nil && !E.IsClosed(err) {
log.Error(E.Cause(err, "serve debug HTTP server"))
}
}()
}

View File

@@ -1,188 +1,3 @@
#### 1.3.5
* Fixes and improvements
* Introducing our [Apple tvOS](/installation/clients/sft) client applications **1**
* Add per app proxy and app installed/updated trigger support for Android client
* Add profile sharing support for Android/iOS/macOS clients
**1**:
Due to the requirement of tvOS 17, the app cannot be submitted to the App Store for the time being, and can only be downloaded through TestFlight.
#### 1.3.4
* Fixes and improvements
* We're now on the [App Store](https://apps.apple.com/us/app/sing-box/id6451272673), always free! It should be noted that due to stricter and slower review, the release of Store versions will be delayed.
* We've made a standalone version of the macOS client (the original Application Extension relies on App Store distribution), which you can download as SFM-version-universal.zip in the release artifacts.
#### 1.3.3
* Fixes and improvements
#### 1.3.1-rc.1
* Fix bugs and update dependencies
#### 1.3.1-beta.3
* Introducing our [new iOS](/installation/clients/sfi) and [macOS](/installation/clients/sfm) client applications **1**
* Fixes and improvements
**1**:
The old testflight link and app are no longer valid.
#### 1.3.1-beta.2
* Fix bugs and update dependencies
#### 1.3.1-beta.1
* Fixes and improvements
#### 1.3.0
* Fix bugs and update dependencies
Important changes since 1.2:
* Add [FakeIP](/configuration/dns/fakeip) support **1**
* Improve multiplex **2**
* Add [DNS reverse mapping](/configuration/dns#reverse_mapping) support
* Add `rewrite_ttl` DNS rule action
* Add `store_fakeip` Clash API option
* Add multi-peer support for [WireGuard](/configuration/outbound/wireguard#peers) outbound
* Add loopback detect
* Add Clash.Meta API compatibility for Clash API
* Download Yacd-meta by default if the specified Clash `external_ui` directory is empty
* Add path and headers option for HTTP outbound
* Perform URLTest recheck after network changes
* Fix `system` tun stack for ios
* Fix network monitor for android/ios
* Update VLESS and XUDP protocol
* Make splice work with traffic statistics systems like Clash API
* Significantly reduces memory usage of idle connections
* Improve DNS caching
* Add `independent_cache` [option](/configuration/dns#independent_cache) for DNS
* Reimplemented shadowsocks client
* Add multiplex support for VLESS outbound
* Automatically add Windows firewall rules in order for the system tun stack to work
* Fix TLS 1.2 support for shadow-tls client
* Add `cache_id` [option](/configuration/experimental#cache_id) for Clash cache file
* Fix `local` DNS transport for Android
*1*:
See [FAQ](/faq/fakeip) for more information.
*2*:
Added new `h2mux` multiplex protocol and `padding` multiplex option, see [Multiplex](/configuration/shared/multiplex).
#### 1.3-rc2
* Fix `local` DNS transport for Android
* Fix bugs and update dependencies
#### 1.3-rc1
* Fix bugs and update dependencies
#### 1.3-beta14
* Fixes and improvements
#### 1.3-beta13
* Fix resolving fakeip domains **1**
* Deprecate L3 routing
* Fix bugs and update dependencies
**1**:
If the destination address of the connection is obtained from fakeip, dns rules with server type fakeip will be skipped.
#### 1.3-beta12
* Automatically add Windows firewall rules in order for the system tun stack to work
* Fix TLS 1.2 support for shadow-tls client
* Add `cache_id` [option](/configuration/experimental#cache_id) for Clash cache file
* Fixes and improvements
#### 1.3-beta11
* Fix bugs and update dependencies
#### 1.3-beta10
* Improve direct copy **1**
* Improve DNS caching
* Add `independent_cache` [option](/configuration/dns#independent_cache) for DNS
* Reimplemented shadowsocks client **2**
* Add multiplex support for VLESS outbound
* Set TCP keepalive for WireGuard gVisor TCP connections
* Fixes and improvements
**1**:
* Make splice work with traffic statistics systems like Clash API
* Significantly reduces memory usage of idle connections
**2**:
Improved performance and reduced memory usage.
#### 1.3-beta9
* Improve multiplex **1**
* Fixes and improvements
*1*:
Added new `h2mux` multiplex protocol and `padding` multiplex option, see [Multiplex](/configuration/shared/multiplex).
#### 1.2.6
* Fix bugs and update dependencies
#### 1.3-beta8
* Fix `system` tun stack for ios
* Fix network monitor for android/ios
* Update VLESS and XUDP protocol **1**
* Fixes and improvements
*1:
This is an incompatible update for XUDP in VLESS if vision flow is enabled.
#### 1.3-beta7
* Add `path` and `headers` options for HTTP outbound
* Add multi-user support for Shadowsocks legacy AEAD inbound
* Fixes and improvements
#### 1.2.4
* Fixes and improvements
#### 1.3-beta6
* Fix WireGuard reconnect
* Perform URLTest recheck after network changes
* Fix bugs and update dependencies
#### 1.3-beta5
* Add Clash.Meta API compatibility for Clash API
* Download Yacd-meta by default if the specified Clash `external_ui` directory is empty
* Add path and headers option for HTTP outbound
* Fixes and improvements
#### 1.3-beta4
* Fix bugs
#### 1.3-beta2 #### 1.3-beta2
* Download clash-dashboard if the specified Clash `external_ui` directory is empty * Download clash-dashboard if the specified Clash `external_ui` directory is empty

View File

@@ -11,7 +11,6 @@
"strategy": "", "strategy": "",
"disable_cache": false, "disable_cache": false,
"disable_expire": false, "disable_expire": false,
"independent_cache": false,
"reverse_mapping": false, "reverse_mapping": false,
"fakeip": {} "fakeip": {}
} }
@@ -49,10 +48,6 @@ Disable dns cache.
Disable dns cache expire. Disable dns cache expire.
#### independent_cache
Make each DNS server's cache independent for special purposes. If enabled, will slightly degrade performance.
#### reverse_mapping #### reverse_mapping
Stores a reverse mapping of IP addresses after responding to a DNS query in order to provide domain names when routing. Stores a reverse mapping of IP addresses after responding to a DNS query in order to provide domain names when routing.

View File

@@ -11,7 +11,6 @@
"strategy": "", "strategy": "",
"disable_cache": false, "disable_cache": false,
"disable_expire": false, "disable_expire": false,
"independent_cache": false,
"reverse_mapping": false, "reverse_mapping": false,
"fakeip": {} "fakeip": {}
} }
@@ -48,10 +47,6 @@
禁用 DNS 缓存过期。 禁用 DNS 缓存过期。
#### independent_cache
使每个 DNS 服务器的缓存独立,以满足特殊目的。如果启用,将轻微降低性能。
#### reverse_mapping #### reverse_mapping
在响应 DNS 查询后存储 IP 地址的反向映射以为路由目的提供域名。 在响应 DNS 查询后存储 IP 地址的反向映射以为路由目的提供域名。

View File

@@ -30,18 +30,18 @@ The tag of the dns server.
The address of the dns server. The address of the dns server.
| Protocol | Format | | Protocol | Format |
|-------------------------------------|-------------------------------| |---------------------|-------------------------------|
| `System` | `local` | | `System` | `local` |
| `TCP` | `tcp://1.0.0.1` | | `TCP` | `tcp://1.0.0.1` |
| `UDP` | `8.8.8.8` `udp://8.8.4.4` | | `UDP` | `8.8.8.8` `udp://8.8.4.4` |
| `TLS` | `tls://dns.google` | | `TLS` | `tls://dns.google` |
| `HTTPS` | `https://1.1.1.1/dns-query` | | `HTTPS` | `https://1.1.1.1/dns-query` |
| `QUIC` | `quic://dns.adguard.com` | | `QUIC` | `quic://dns.adguard.com` |
| `HTTP3` | `h3://8.8.8.8/dns-query` | | `HTTP3` | `h3://8.8.8.8/dns-query` |
| `RCode` | `rcode://refused` | | `RCode` | `rcode://refused` |
| `DHCP` | `dhcp://auto` or `dhcp://en0` | | `DHCP` | `dhcp://auto` or `dhcp://en0` |
| [FakeIP](/configuration/dns/fakeip) | `fakeip` | | [FakeIP](./fakeip) | `fakeip` |
!!! warning "" !!! warning ""
@@ -94,4 +94,4 @@ Take no effect if override by other settings.
Tag of an outbound for connecting to the dns server. Tag of an outbound for connecting to the dns server.
Default outbound will be used if empty. Default outbound will be used if empty.

View File

@@ -30,18 +30,18 @@ DNS 服务器的标签。
DNS 服务器的地址。 DNS 服务器的地址。
| 协议 | 格式 | | 协议 | 格式 |
|-------------------------------------|------------------------------| |--------------------|------------------------------|
| `System` | `local` | | `System` | `local` |
| `TCP` | `tcp://1.0.0.1` | | `TCP` | `tcp://1.0.0.1` |
| `UDP` | `8.8.8.8` `udp://8.8.4.4` | | `UDP` | `8.8.8.8` `udp://8.8.4.4` |
| `TLS` | `tls://dns.google` | | `TLS` | `tls://dns.google` |
| `HTTPS` | `https://1.1.1.1/dns-query` | | `HTTPS` | `https://1.1.1.1/dns-query` |
| `QUIC` | `quic://dns.adguard.com` | | `QUIC` | `quic://dns.adguard.com` |
| `HTTP3` | `h3://8.8.8.8/dns-query` | | `HTTP3` | `h3://8.8.8.8/dns-query` |
| `RCode` | `rcode://refused` | | `RCode` | `rcode://refused` |
| `DHCP` | `dhcp://auto``dhcp://en0` | | `DHCP` | `dhcp://auto``dhcp://en0` |
| [FakeIP](/configuration/dns/fakeip) | `fakeip` | | [FakeIP](./fakeip) | `fakeip` |
!!! warning "" !!! warning ""

View File

@@ -7,14 +7,11 @@
"experimental": { "experimental": {
"clash_api": { "clash_api": {
"external_controller": "127.0.0.1:9090", "external_controller": "127.0.0.1:9090",
"external_ui": "", "external_ui": "folder",
"external_ui_download_url": "",
"external_ui_download_detour": "",
"secret": "", "secret": "",
"default_mode": "", "default_mode": "rule",
"store_selected": false, "store_selected": false,
"cache_file": "", "cache_file": "cache.db"
"cache_id": ""
}, },
"v2ray_api": { "v2ray_api": {
"listen": "127.0.0.1:8080", "listen": "127.0.0.1:8080",
@@ -56,18 +53,6 @@ A relative path to the configuration directory or an absolute path to a
directory in which you put some static web resource. sing-box will then directory in which you put some static web resource. sing-box will then
serve it at `http://{{external-controller}}/ui`. serve it at `http://{{external-controller}}/ui`.
#### external_ui_download_url
ZIP download URL for the external UI, will be used if the specified `external_ui` directory is empty.
`https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip` will be used if empty.
#### external_ui_download_detour
The tag of the outbound to download the external UI.
Default outbound will be used if empty.
#### secret #### secret
Secret for the RESTful API (optional) Secret for the RESTful API (optional)
@@ -92,12 +77,6 @@ Store selected outbound for the `Selector` outbound in cache file.
Cache file path, `cache.db` will be used if empty. Cache file path, `cache.db` will be used if empty.
#### cache_id
Cache ID.
If not empty, `store_selected` will use a separate store keyed by it.
### V2Ray API Fields ### V2Ray API Fields
!!! error "" !!! error ""

View File

@@ -7,14 +7,11 @@
"experimental": { "experimental": {
"clash_api": { "clash_api": {
"external_controller": "127.0.0.1:9090", "external_controller": "127.0.0.1:9090",
"external_ui": "", "external_ui": "folder",
"external_ui_download_url": "",
"external_ui_download_detour": "",
"secret": "", "secret": "",
"default_mode": "", "default_mode": "rule",
"store_selected": false, "store_selected": false,
"cache_file": "", "cache_file": "cache.db"
"cache_id": ""
}, },
"v2ray_api": { "v2ray_api": {
"listen": "127.0.0.1:8080", "listen": "127.0.0.1:8080",
@@ -54,18 +51,6 @@ RESTful web API 监听地址。如果为空,则禁用 Clash API。
到静态网页资源目录的相对路径或绝对路径。sing-box 会在 `http://{{external-controller}}/ui` 下提供它。 到静态网页资源目录的相对路径或绝对路径。sing-box 会在 `http://{{external-controller}}/ui` 下提供它。
#### external_ui_download_url
静态网页资源的 ZIP 下载 URL如果指定的 `external_ui` 目录为空,将使用。
默认使用 `https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip`
#### external_ui_download_detour
用于下载静态网页资源的出站的标签。
如果为空,将使用默认出站。
#### secret #### secret
RESTful API 的密钥(可选) RESTful API 的密钥(可选)
@@ -90,12 +75,6 @@ Clash 中的默认模式,默认使用 `rule`。
缓存文件路径,默认使用`cache.db` 缓存文件路径,默认使用`cache.db`
#### cache_id
缓存 ID。
如果不为空,`store_selected` 将会使用以此为键的独立存储。
### V2Ray API 字段 ### V2Ray API 字段
!!! error "" !!! error ""

View File

@@ -11,8 +11,6 @@
"server_port": 1080, "server_port": 1080,
"username": "sekai", "username": "sekai",
"password": "admin", "password": "admin",
"path": "",
"headers": {},
"tls": {}, "tls": {},
... // Dial Fields ... // Dial Fields
@@ -41,14 +39,6 @@ Basic authorization username.
Basic authorization password. Basic authorization password.
#### path
Path of HTTP request.
#### headers
Extra headers of HTTP request.
#### tls #### tls
TLS configuration, see [TLS](/configuration/shared/tls/#outbound). TLS configuration, see [TLS](/configuration/shared/tls/#outbound).

View File

@@ -11,8 +11,6 @@
"server_port": 1080, "server_port": 1080,
"username": "sekai", "username": "sekai",
"password": "admin", "password": "admin",
"path": "",
"headers": {},
"tls": {}, "tls": {},
... // 拨号字段 ... // 拨号字段
@@ -41,14 +39,6 @@ Basic 认证用户名。
Basic 认证密码。 Basic 认证密码。
#### path
HTTP 请求路径。
#### headers
HTTP 请求的额外标头。
#### tls #### tls
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。 TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。

View File

@@ -10,7 +10,7 @@
"proxy-b", "proxy-b",
"proxy-c" "proxy-c"
], ],
"url": "https://www.gstatic.com/generate_204", "url": "http://www.gstatic.com/generate_204",
"interval": "1m", "interval": "1m",
"tolerance": 50 "tolerance": 50
} }
@@ -26,7 +26,7 @@ List of outbound tags to test.
#### url #### url
The URL to test. `https://www.gstatic.com/generate_204` will be used if empty. The URL to test. `http://www.gstatic.com/generate_204` will be used if empty.
#### interval #### interval

View File

@@ -10,7 +10,7 @@
"proxy-b", "proxy-b",
"proxy-c" "proxy-c"
], ],
"url": "https://www.gstatic.com/generate_204", "url": "http://www.gstatic.com/generate_204",
"interval": "1m", "interval": "1m",
"tolerance": 50 "tolerance": 50
} }
@@ -26,7 +26,7 @@
#### url #### url
用于测试的链接。默认使用 `https://www.gstatic.com/generate_204` 用于测试的链接。默认使用 `http://www.gstatic.com/generate_204`
#### interval #### interval

View File

@@ -7,6 +7,7 @@
"route": { "route": {
"geoip": {}, "geoip": {},
"geosite": {}, "geosite": {},
"ip_rules": [],
"rules": [], "rules": [],
"final": "", "final": "",
"auto_detect_interface": false, "auto_detect_interface": false,
@@ -23,6 +24,7 @@
|------------|------------------------------------| |------------|------------------------------------|
| `geoip` | [GeoIP](./geoip) | | `geoip` | [GeoIP](./geoip) |
| `geosite` | [Geosite](./geosite) | | `geosite` | [Geosite](./geosite) |
| `ip_rules` | List of [IP Route Rule](./ip-rule) |
| `rules` | List of [Route Rule](./rule) | | `rules` | List of [Route Rule](./rule) |
#### final #### final

View File

@@ -10,8 +10,7 @@
"protocol": "smux", "protocol": "smux",
"max_connections": 4, "max_connections": 4,
"min_streams": 4, "min_streams": 4,
"max_streams": 0, "max_streams": 0
"padding": false
} }
``` ```
@@ -29,9 +28,8 @@ Multiplex protocol.
|----------|------------------------------------| |----------|------------------------------------|
| smux | https://github.com/xtaci/smux | | smux | https://github.com/xtaci/smux |
| yamux | https://github.com/hashicorp/yamux | | yamux | https://github.com/hashicorp/yamux |
| h2mux | https://golang.org/x/net/http2 |
h2mux is used by default. SMux is used by default.
#### max_connections #### max_connections
@@ -50,12 +48,3 @@ Conflict with `max_streams`.
Maximum multiplexed streams in a connection before opening a new connection. Maximum multiplexed streams in a connection before opening a new connection.
Conflict with `max_connections` and `min_streams`. Conflict with `max_connections` and `min_streams`.
#### padding
!!! info
Requires sing-box server version 1.3-beta9 or later.
Enable padding.

View File

@@ -28,9 +28,8 @@
|-------|------------------------------------| |-------|------------------------------------|
| smux | https://github.com/xtaci/smux | | smux | https://github.com/xtaci/smux |
| yamux | https://github.com/hashicorp/yamux | | yamux | https://github.com/hashicorp/yamux |
| h2mux | https://golang.org/x/net/http2 |
默认使用 h2mux。 默认使用 SMux。
#### max_connections #### max_connections
@@ -48,13 +47,4 @@
在打开新连接之前,连接中的最大多路复用流数量。 在打开新连接之前,连接中的最大多路复用流数量。
`max_connections``min_streams` 冲突。 `max_connections``min_streams` 冲突。
#### padding
!!! info
需要 sing-box 服务器版本 1.3-beta9 或更高。
启用填充。

View File

@@ -1,106 +0,0 @@
```json
{
"dns": {
"servers": [
{
"tag": "google",
"address": "tls://8.8.8.8"
},
{
"tag": "local",
"address": "223.5.5.5",
"detour": "direct"
},
{
"tag": "remote",
"address": "fakeip"
},
{
"tag": "block",
"address": "rcode://success"
}
],
"rules": [
{
"geosite": "category-ads-all",
"server": "block",
"disable_cache": true
},
{
"outbound": "any",
"server": "local"
},
{
"geosite": "cn",
"server": "local"
},
{
"query_type": [
"A",
"AAAA"
],
"server": "remote"
}
],
"fakeip": {
"enabled": true,
"inet4_range": "198.18.0.0/15",
"inet6_range": "fc00::/18"
},
"independent_cache": true,
"strategy": "ipv4_only"
},
"inbounds": [
{
"type": "tun",
"inet4_address": "172.19.0.1/30",
"auto_route": true,
"sniff": true,
"domain_strategy": "ipv4_only" // remove this line if you want to resolve the domain remotely (if the server is not sing-box, UDP may not work due to wrong behavior).
}
],
"outbounds": [
{
"type": "shadowsocks",
"tag": "proxy",
"server": "mydomain.com",
"server_port": 8080,
"method": "2022-blake3-aes-128-gcm",
"password": "8JCsPssfgS8tiRwiMlhARg=="
},
{
"type": "direct",
"tag": "direct"
},
{
"type": "block",
"tag": "block"
},
{
"type": "dns",
"tag": "dns-out"
}
],
"route": {
"rules": [
{
"protocol": "dns",
"outbound": "dns-out"
},
{
"geosite": "cn",
"geoip": [
"private",
"cn"
],
"outbound": "direct"
},
{
"geosite": "category-ads-all",
"outbound": "block"
}
],
"auto_detect_interface": true
}
}
```

View File

@@ -1,106 +0,0 @@
```json
{
"dns": {
"servers": [
{
"tag": "google",
"address": "tls://8.8.8.8"
},
{
"tag": "local",
"address": "223.5.5.5",
"detour": "direct"
},
{
"tag": "remote",
"address": "fakeip"
},
{
"tag": "block",
"address": "rcode://success"
}
],
"rules": [
{
"geosite": "category-ads-all",
"server": "block",
"disable_cache": true
},
{
"outbound": "any",
"server": "local"
},
{
"geosite": "cn",
"server": "local"
},
{
"query_type": [
"A",
"AAAA"
],
"server": "remote"
}
],
"fakeip": {
"enabled": true,
"inet4_range": "198.18.0.0/15",
"inet6_range": "fc00::/18"
},
"independent_cache": true,
"strategy": "ipv4_only"
},
"inbounds": [
{
"type": "tun",
"inet4_address": "172.19.0.1/30",
"auto_route": true,
"sniff": true,
"domain_strategy": "ipv4_only" // 如果您想在远程解析域,删除此行 (如果服务器程序不为 sing-box可能由于错误的行为导致 UDP 无法使用)。
}
],
"outbounds": [
{
"type": "shadowsocks",
"tag": "proxy",
"server": "mydomain.com",
"server_port": 8080,
"method": "2022-blake3-aes-128-gcm",
"password": "8JCsPssfgS8tiRwiMlhARg=="
},
{
"type": "direct",
"tag": "direct"
},
{
"type": "block",
"tag": "block"
},
{
"type": "dns",
"tag": "dns-out"
}
],
"route": {
"rules": [
{
"protocol": "dns",
"outbound": "dns-out"
},
{
"geosite": "cn",
"geoip": [
"private",
"cn"
],
"outbound": "direct"
},
{
"geosite": "category-ads-all",
"outbound": "block"
}
],
"auto_detect_interface": true
}
}
```

View File

@@ -9,4 +9,3 @@ Configuration examples for sing-box.
* [ShadowTLS](./shadowtls) * [ShadowTLS](./shadowtls)
* [Clash API](./clash-api) * [Clash API](./clash-api)
* [WireGuard Direct](./wireguard-direct) * [WireGuard Direct](./wireguard-direct)
* [FakeIP](./fakeip)

View File

@@ -9,4 +9,3 @@ sing-box 的配置示例。
* [ShadowTLS](./shadowtls) * [ShadowTLS](./shadowtls)
* [Clash API](./clash-api) * [Clash API](./clash-api)
* [WireGuard Direct](./wireguard-direct) * [WireGuard Direct](./wireguard-direct)
* [FakeIP](./fakeip)

View File

@@ -5,7 +5,8 @@ responds to DNS requests with virtual results and restores mapping when acceptin
#### Advantage #### Advantage
* * Retrieve the requested domain in places like IP routing (L3) where traffic detection is not possible to assist with routing.
* Decrease an RTT on the first TCP request to a domain (the most common reason).
#### Limitation #### Limitation
@@ -14,6 +15,6 @@ responds to DNS requests with virtual results and restores mapping when acceptin
#### Recommendation #### Recommendation
* Enable `dns.independent_cache` unless you always resolve FakeIP domains remotely. * Do not use if you do not need L3 routing.
* If using tun, make sure FakeIP ranges is included in the tun's routes. * If using tun, make sure FakeIP ranges is included in the tun's routes.
* Enable `experimental.clash_api.store_fakeip` to persist FakeIP records, or use `dns.rules.rewrite_ttl` to avoid losing records after program restart in DNS cached environments. * Enable `experimental.clash_api.store_fakeip` to persist FakeIP records, or use `dns.rules.rewrite_ttl` to avoid losing records after program restart in DNS cached environments.

View File

@@ -4,7 +4,8 @@ FakeIP 是指同时劫持 DNS 和连接请求的程序中的一种行为。它
#### 优点 #### 优点
* * 在像 L3 路由这样无法进行流量探测的地方检索所请求的域名,以协助路由。
* 减少对一个域的第一个 TCP 请求的 RTT这是最常见的原因
#### 限制 #### 限制
@@ -13,6 +14,6 @@ FakeIP 是指同时劫持 DNS 和连接请求的程序中的一种行为。它
#### 建议 #### 建议
* 启用 `dns.independent_cache` 除非您始终远程解析 FakeIP 域 * 如果不需要 L3 路由,请勿使用
* 如果使用 tun请确保 tun 路由中包含 FakeIP 地址范围。 * 如果使用 tun请确保 tun 路由中包含 FakeIP 地址范围。
* 启用 `experimental.clash_api.store_fakeip` 以持久化 FakeIP 记录,或者使用 `dns.rules.rewrite_ttl` 避免程序重启后在 DNS 被缓存的环境中丢失记录。 * 启用 `experimental.clash_api.store_fakeip` 以持久化 FakeIP 记录,或者使用 `dns.rules.rewrite_ttl` 避免程序重启后在 DNS 被缓存的环境中丢失记录。

View File

@@ -12,6 +12,5 @@ Experimental Android client for sing-box.
#### Note #### Note
* User Agent in remote profile request is `SFA/$version ($version_code; sing-box $sing_box_version)` * Working directory is at `/sdcard/Android/data/io.nekohasekai.sfa/files` (External files directory)
* The working directory is located at `/sdcard/Android/data/io.nekohasekai.sfa/files` (External files directory) * User Agent is `SFA/$version ($version_code; sing-box $sing_box_version)` in the remote profile request
* Crash logs is located in `$working_directory/stderr.log`

View File

@@ -12,6 +12,5 @@
#### 注意事项 #### 注意事项
* 远程配置文件请求中的 User Agent 为 `SFA/$version ($version_code; sing-box $sing_box_version)`
* 工作目录位于 `/sdcard/Android/data/io.nekohasekai.sfa/files` (外部文件目录) * 工作目录位于 `/sdcard/Android/data/io.nekohasekai.sfa/files` (外部文件目录)
* 崩溃日志位于 `$working_directory/stderr.log` * 远程配置文件请求中的 User Agent 为 `SFA/$version ($version_code; sing-box $sing_box_version)`

View File

@@ -5,17 +5,16 @@ Experimental iOS client for sing-box.
#### Requirements #### Requirements
* iOS 15.0+ * iOS 15.0+
* An Apple account outside of mainland China * macOS 12.0+ with Apple Silicon
#### Download #### Download
* [AppStore](https://apps.apple.com/us/app/sing-box/id6451272673) * [TestFlight](https://testflight.apple.com/join/c6ylui2j)
* [TestFlight](https://testflight.apple.com/join/AcqO44FH)
#### Note #### Note
* User Agent in remote profile request is `SFI/$version ($version_code; sing-box $sing_box_version)` * `system` tun stack not working on iOS
* Crash logs is located in `Settings` -> `View Service Log` * User Agent is `SFI/$version ($version_code; sing-box $sing_box_version)` in the remote profile request
#### Privacy policy #### Privacy policy

View File

@@ -5,17 +5,16 @@
#### 要求 #### 要求
* iOS 15.0+ * iOS 15.0+
* 一个非中国大陆地区的 Apple 账号 * macOS 12.0+ with Apple Silicon
#### 下载 #### 下载
* [AppStore](https://apps.apple.com/us/app/sing-box/id6451272673) * [TestFlight](https://testflight.apple.com/join/c6ylui2j)
* [TestFlight](https://testflight.apple.com/join/AcqO44FH)
#### 注意事项 #### 注意事项
* `system` tun stack 在 iOS 不工作
* 远程配置文件请求中的 User Agent 为 `SFI/$version ($version_code; sing-box $sing_box_version)` * 远程配置文件请求中的 User Agent 为 `SFI/$version ($version_code; sing-box $sing_box_version)`
* 崩溃日志位于 `设置` -> `查看服务日志`
#### 隐私政策 #### 隐私政策

View File

@@ -1,29 +0,0 @@
# SFM
Experimental macOS client for sing-box.
#### Requirements
* macOS 13.0+
* An Apple account outside of mainland China (App Store Version)
#### Download (App Store Version)
* [AppStore](https://apps.apple.com/us/app/sing-box/id6451272673)
* [TestFlight](https://testflight.apple.com/join/AcqO44FH)
#### Download (Independent Version)
* [GitHub Release](https://github.com/SagerNet/sing-box/releases/latest)
* Homebrew (Cask): `brew install sfm`
* Homebrew (Tap): `brew tap sagernet/sing-box && brew install sagernet/sing-box/sfm`
#### Note
* User Agent in remote profile request is `SFM/$version ($version_code; sing-box $sing_box_version)`
* Crash logs is located in `Settings` -> `View Service Log`
#### Privacy policy
* SFM did not collect or share personal data.
* The data generated by the software is always on your device.

View File

@@ -1,29 +0,0 @@
# SFM
实验性的 macOS sing-box 客户端。
#### 要求
* macOS 13.0+
* 一个非中国大陆地区的 Apple 账号 (商店版本)
#### 下载 (商店版本)
* [AppStore](https://apps.apple.com/us/app/sing-box/id6451272673)
* [TestFlight](https://testflight.apple.com/join/AcqO44FH)
#### 下载 (独立版本)
* [GitHub Release](https://github.com/SagerNet/sing-box/releases/latest)
* Homebrew (Cask): `brew install sfm`
* Homebrew (Tap): `brew tap sagernet/sing-box && brew install sagernet/sing-box/sfm`
#### 注意事项
* 远程配置文件请求中的 User Agent 为 `SFM/$version ($version_code; sing-box $sing_box_version)`
* 崩溃日志位于 `设置` -> `查看服务日志`
#### 隐私政策
* SFM 不收集或共享个人数据。
* 软件生成的数据始终在您的设备上。

View File

@@ -1,29 +0,0 @@
# SFT
Experimental Apple tvOS client for sing-box.
#### Requirements
* tvOS 17.0+
#### Download
* [TestFlight](https://testflight.apple.com/join/AcqO44FH)
#### Features
Full functionality, except for:
* Only remote configuration files can be created manually
* You need to update SFI to the latest beta version to import profiles from iPhone/iPad
* No iCloud profile support
#### Note
* User Agent in remote profile request is `SFT/$version ($version_code; sing-box $sing_box_version)`
* Crash logs is located in `Settings` -> `View Service Log`
#### Privacy policy
* SFT did not collect or share personal data.
* The data generated by the software is always on your device.

View File

@@ -1,25 +0,0 @@
# Specification
## Profile
Profile defines a sing-box configuration with metadata in a GUI client.
## Profile Types
### Local
Create a empty configuration or import from a local file.
### iCloud (on Apple platforms)
Create a new configuration or use an existing configuration on iCloud.
### Remote
Use a remote URL as the configuration source, with HTTP basic authentication and automatic update support.
#### URL specification
```
sing-box://import-remote-profile?url=urlEncodedURL#urlEncodedName
```

View File

@@ -1,7 +0,0 @@
# Android
## Termux
```shell
pkg add sing-box
```

View File

@@ -1,14 +0,0 @@
# macOS
## Homebrew (core)
```shell
brew install sing-box
```
## Homebrew (Tap)
```shell
brew tap sagernet/sing-box
brew install sagernet/sing-box/sing-box
```

View File

@@ -1,13 +0,0 @@
# Windows
## Chocolatey
```shell
choco install sing-box
```
## winget
```shell
winget install sing-box
```

View File

@@ -1,4 +1,8 @@
Github Issue: [Issues · SagerNet/sing-box](https://github.com/SagerNet/sing-box/issues) #### Github
Telegram Notification channel: [@yapnc](https://t.me/yapnc)
Telegram User group: [@yapug](https://t.me/yapug) Issue: [Issues · SagerNet/sing-box](https://github.com/SagerNet/sing-box/issues)
Email: [contact@sagernet.org](mailto:contact@sagernet.org)
#### Telegram
Notification channel: [@yapnc](https://t.me/yapnc)
User group: [@yapug](https://t.me/yapug)

View File

@@ -1,4 +1,8 @@
Github 工单: [Issues · SagerNet/sing-box](https://github.com/SagerNet/sing-box/issues) #### Github
Telegram 通知频道: [@yapnc](https://t.me/yapnc)
Telegram 用户组: [@yapug](https://t.me/yapug) 工单: [Issues · SagerNet/sing-box](https://github.com/SagerNet/sing-box/issues)
Email: [contact@sagernet.org](mailto:contact@sagernet.org)
#### Telegram
通知频道: [@yapnc](https://t.me/yapnc)
用户组: [@yapug](https://t.me/yapug)

View File

@@ -1,7 +1,6 @@
package experimental package experimental
import ( import (
"context"
"os" "os"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
@@ -9,7 +8,7 @@ import (
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
) )
type ClashServerConstructor = func(ctx context.Context, router adapter.Router, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.ClashServer, error) type ClashServerConstructor = func(router adapter.Router, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.ClashServer, error)
var clashServerConstructor ClashServerConstructor var clashServerConstructor ClashServerConstructor
@@ -17,9 +16,9 @@ func RegisterClashServerConstructor(constructor ClashServerConstructor) {
clashServerConstructor = constructor clashServerConstructor = constructor
} }
func NewClashServer(ctx context.Context, router adapter.Router, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.ClashServer, error) { func NewClashServer(router adapter.Router, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.ClashServer, error) {
if clashServerConstructor == nil { if clashServerConstructor == nil {
return nil, os.ErrInvalid return nil, os.ErrInvalid
} }
return clashServerConstructor(ctx, router, logFactory, options) return clashServerConstructor(router, logFactory, options)
} }

View File

@@ -1,78 +0,0 @@
package clashapi
import (
"bytes"
"net/http"
"time"
"github.com/sagernet/sing-box/common/json"
"github.com/sagernet/sing-box/experimental/clashapi/trafficontrol"
"github.com/sagernet/websocket"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
)
// API created by Clash.Meta
func (s *Server) setupMetaAPI(r chi.Router) {
r.Get("/memory", memory(s.trafficManager))
r.Mount("/group", groupRouter(s))
}
type Memory struct {
Inuse uint64 `json:"inuse"`
OSLimit uint64 `json:"oslimit"` // maybe we need it in the future
}
func memory(trafficManager *trafficontrol.Manager) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var wsConn *websocket.Conn
if websocket.IsWebSocketUpgrade(r) {
var err error
wsConn, err = upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
}
if wsConn == nil {
w.Header().Set("Content-Type", "application/json")
render.Status(r, http.StatusOK)
}
tick := time.NewTicker(time.Second)
defer tick.Stop()
buf := &bytes.Buffer{}
var err error
first := true
for range tick.C {
buf.Reset()
inuse := trafficManager.Snapshot().Memory
// make chat.js begin with zero
// this is shit var,but we need output 0 for first time
if first {
first = false
inuse = 0
}
if err := json.NewEncoder(buf).Encode(Memory{
Inuse: inuse,
OSLimit: 0,
}); err != nil {
break
}
if wsConn == nil {
_, err = w.Write(buf.Bytes())
w.(http.Flusher).Flush()
} else {
err = wsConn.WriteMessage(websocket.TextMessage, buf.Bytes())
}
if err != nil {
break
}
}
}
}

View File

@@ -1,136 +0,0 @@
package clashapi
import (
"context"
"net/http"
"strconv"
"strings"
"sync"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/badjson"
"github.com/sagernet/sing-box/common/urltest"
"github.com/sagernet/sing-box/outbound"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/batch"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
)
func groupRouter(server *Server) http.Handler {
r := chi.NewRouter()
r.Get("/", getGroups(server))
r.Route("/{name}", func(r chi.Router) {
r.Use(parseProxyName, findProxyByName(server.router))
r.Get("/", getGroup(server))
r.Get("/delay", getGroupDelay(server))
})
return r
}
func getGroups(server *Server) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
groups := common.Map(common.Filter(server.router.Outbounds(), func(it adapter.Outbound) bool {
_, isGroup := it.(adapter.OutboundGroup)
return isGroup
}), func(it adapter.Outbound) *badjson.JSONObject {
return proxyInfo(server, it)
})
render.JSON(w, r, render.M{
"proxies": groups,
})
}
}
func getGroup(server *Server) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
proxy := r.Context().Value(CtxKeyProxy).(adapter.Outbound)
if _, ok := proxy.(adapter.OutboundGroup); ok {
render.JSON(w, r, proxyInfo(server, proxy))
return
}
render.Status(r, http.StatusNotFound)
render.JSON(w, r, ErrNotFound)
}
}
func getGroupDelay(server *Server) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
proxy := r.Context().Value(CtxKeyProxy).(adapter.Outbound)
group, ok := proxy.(adapter.OutboundGroup)
if !ok {
render.Status(r, http.StatusNotFound)
render.JSON(w, r, ErrNotFound)
return
}
query := r.URL.Query()
url := query.Get("url")
if strings.HasPrefix(url, "http://") {
url = ""
}
timeout, err := strconv.ParseInt(query.Get("timeout"), 10, 32)
if err != nil {
render.Status(r, http.StatusBadRequest)
render.JSON(w, r, ErrBadRequest)
return
}
ctx, cancel := context.WithTimeout(r.Context(), time.Millisecond*time.Duration(timeout))
defer cancel()
var result map[string]uint16
if urlTestGroup, isURLTestGroup := group.(adapter.URLTestGroup); isURLTestGroup {
result, err = urlTestGroup.URLTest(ctx, url)
} else {
outbounds := common.FilterNotNil(common.Map(group.All(), func(it string) adapter.Outbound {
itOutbound, _ := server.router.Outbound(it)
return itOutbound
}))
b, _ := batch.New(ctx, batch.WithConcurrencyNum[any](10))
checked := make(map[string]bool)
result = make(map[string]uint16)
var resultAccess sync.Mutex
for _, detour := range outbounds {
tag := detour.Tag()
realTag := outbound.RealTag(detour)
if checked[realTag] {
continue
}
checked[realTag] = true
p, loaded := server.router.Outbound(realTag)
if !loaded {
continue
}
b.Go(realTag, func() (any, error) {
t, err := urltest.URLTest(ctx, url, p)
if err != nil {
server.logger.Debug("outbound ", tag, " unavailable: ", err)
server.urlTestHistory.DeleteURLTestHistory(realTag)
} else {
server.logger.Debug("outbound ", tag, " available: ", t, "ms")
server.urlTestHistory.StoreURLTestHistory(realTag, &urltest.History{
Time: time.Now(),
Delay: t,
})
resultAccess.Lock()
result[tag] = t
resultAccess.Unlock()
}
return nil, nil
})
}
b.Wait()
}
if err != nil {
render.Status(r, http.StatusGatewayTimeout)
render.JSON(w, r, newError(err.Error()))
return
}
render.JSON(w, r, result)
}
}

View File

@@ -1,10 +1,7 @@
package cachefile package cachefile
import ( import (
"net/netip"
"os" "os"
"strings"
"sync"
"time" "time"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
@@ -17,15 +14,10 @@ var bucketSelected = []byte("selected")
var _ adapter.ClashCacheFile = (*CacheFile)(nil) var _ adapter.ClashCacheFile = (*CacheFile)(nil)
type CacheFile struct { type CacheFile struct {
DB *bbolt.DB DB *bbolt.DB
cacheID []byte
saveAccess sync.RWMutex
saveDomain map[netip.Addr]string
saveAddress4 map[string]netip.Addr
saveAddress6 map[string]netip.Addr
} }
func Open(path string, cacheID string) (*CacheFile, error) { func Open(path string) (*CacheFile, error) {
const fileMode = 0o666 const fileMode = 0o666
options := bbolt.Options{Timeout: time.Second} options := bbolt.Options{Timeout: time.Second}
db, err := bbolt.Open(path, fileMode, &options) db, err := bbolt.Open(path, fileMode, &options)
@@ -39,73 +31,13 @@ func Open(path string, cacheID string) (*CacheFile, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
var cacheIDBytes []byte return &CacheFile{db}, nil
if cacheID != "" {
cacheIDBytes = append([]byte{0}, []byte(cacheID)...)
}
err = db.Batch(func(tx *bbolt.Tx) error {
return tx.ForEach(func(name []byte, b *bbolt.Bucket) error {
if name[0] == 0 {
return b.ForEachBucket(func(k []byte) error {
bucketName := string(k)
if !(bucketName == string(bucketSelected)) {
delErr := b.DeleteBucket(name)
if delErr != nil {
return delErr
}
}
return nil
})
} else {
bucketName := string(name)
if !(bucketName == string(bucketSelected) || strings.HasPrefix(bucketName, fakeipBucketPrefix)) {
delErr := tx.DeleteBucket(name)
if delErr != nil {
return delErr
}
}
}
return nil
})
})
if err != nil {
return nil, err
}
return &CacheFile{
DB: db,
cacheID: cacheIDBytes,
saveDomain: make(map[netip.Addr]string),
saveAddress4: make(map[string]netip.Addr),
saveAddress6: make(map[string]netip.Addr),
}, nil
}
func (c *CacheFile) bucket(t *bbolt.Tx, key []byte) *bbolt.Bucket {
if c.cacheID == nil {
return t.Bucket(key)
}
bucket := t.Bucket(c.cacheID)
if bucket == nil {
return nil
}
return bucket.Bucket(key)
}
func (c *CacheFile) createBucket(t *bbolt.Tx, key []byte) (*bbolt.Bucket, error) {
if c.cacheID == nil {
return t.CreateBucketIfNotExists(key)
}
bucket, err := t.CreateBucketIfNotExists(c.cacheID)
if bucket == nil {
return nil, err
}
return bucket.CreateBucketIfNotExists(key)
} }
func (c *CacheFile) LoadSelected(group string) string { func (c *CacheFile) LoadSelected(group string) string {
var selected string var selected string
c.DB.View(func(t *bbolt.Tx) error { c.DB.View(func(t *bbolt.Tx) error {
bucket := c.bucket(t, bucketSelected) bucket := t.Bucket(bucketSelected)
if bucket == nil { if bucket == nil {
return nil return nil
} }
@@ -120,7 +52,7 @@ func (c *CacheFile) LoadSelected(group string) string {
func (c *CacheFile) StoreSelected(group, selected string) error { func (c *CacheFile) StoreSelected(group, selected string) error {
return c.DB.Batch(func(t *bbolt.Tx) error { return c.DB.Batch(func(t *bbolt.Tx) error {
bucket, err := c.createBucket(t, bucketSelected) bucket, err := t.CreateBucketIfNotExists(bucketSelected)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -5,24 +5,18 @@ import (
"os" "os"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
"go.etcd.io/bbolt" "go.etcd.io/bbolt"
) )
const fakeipBucketPrefix = "fakeip_"
var ( var (
bucketFakeIP = []byte(fakeipBucketPrefix + "address") bucketFakeIP = []byte("fakeip")
bucketFakeIPDomain4 = []byte(fakeipBucketPrefix + "domain4") keyMetadata = []byte("metadata")
bucketFakeIPDomain6 = []byte(fakeipBucketPrefix + "domain6")
keyMetadata = []byte(fakeipBucketPrefix + "metadata")
) )
func (c *CacheFile) FakeIPMetadata() *adapter.FakeIPMetadata { func (c *CacheFile) FakeIPMetadata() *adapter.FakeIPMetadata {
var metadata adapter.FakeIPMetadata var metadata adapter.FakeIPMetadata
err := c.DB.Batch(func(tx *bbolt.Tx) error { err := c.DB.View(func(tx *bbolt.Tx) error {
bucket := tx.Bucket(bucketFakeIP) bucket := tx.Bucket(bucketFakeIP)
if bucket == nil { if bucket == nil {
return nil return nil
@@ -31,10 +25,6 @@ func (c *CacheFile) FakeIPMetadata() *adapter.FakeIPMetadata {
if len(metadataBinary) == 0 { if len(metadataBinary) == 0 {
return os.ErrInvalid return os.ErrInvalid
} }
err := bucket.Delete(keyMetadata)
if err != nil {
return err
}
return metadata.UnmarshalBinary(metadataBinary) return metadata.UnmarshalBinary(metadataBinary)
}) })
if err != nil { if err != nil {
@@ -63,54 +53,11 @@ func (c *CacheFile) FakeIPStore(address netip.Addr, domain string) error {
if err != nil { if err != nil {
return err return err
} }
err = bucket.Put(address.AsSlice(), []byte(domain)) return bucket.Put(address.AsSlice(), []byte(domain))
if err != nil {
return err
}
if address.Is4() {
bucket, err = tx.CreateBucketIfNotExists(bucketFakeIPDomain4)
} else {
bucket, err = tx.CreateBucketIfNotExists(bucketFakeIPDomain6)
}
if err != nil {
return err
}
return bucket.Put([]byte(domain), address.AsSlice())
}) })
} }
func (c *CacheFile) FakeIPStoreAsync(address netip.Addr, domain string, logger logger.Logger) {
c.saveAccess.Lock()
c.saveDomain[address] = domain
if address.Is4() {
c.saveAddress4[domain] = address
} else {
c.saveAddress6[domain] = address
}
c.saveAccess.Unlock()
go func() {
err := c.FakeIPStore(address, domain)
if err != nil {
logger.Warn("save FakeIP address pair: ", err)
}
c.saveAccess.Lock()
delete(c.saveDomain, address)
if address.Is4() {
delete(c.saveAddress4, domain)
} else {
delete(c.saveAddress6, domain)
}
c.saveAccess.Unlock()
}()
}
func (c *CacheFile) FakeIPLoad(address netip.Addr) (string, bool) { func (c *CacheFile) FakeIPLoad(address netip.Addr) (string, bool) {
c.saveAccess.RLock()
cachedDomain, cached := c.saveDomain[address]
c.saveAccess.RUnlock()
if cached {
return cachedDomain, true
}
var domain string var domain string
_ = c.DB.View(func(tx *bbolt.Tx) error { _ = c.DB.View(func(tx *bbolt.Tx) error {
bucket := tx.Bucket(bucketFakeIP) bucket := tx.Bucket(bucketFakeIP)
@@ -123,48 +70,8 @@ func (c *CacheFile) FakeIPLoad(address netip.Addr) (string, bool) {
return domain, domain != "" return domain, domain != ""
} }
func (c *CacheFile) FakeIPLoadDomain(domain string, isIPv6 bool) (netip.Addr, bool) {
var (
cachedAddress netip.Addr
cached bool
)
c.saveAccess.RLock()
if !isIPv6 {
cachedAddress, cached = c.saveAddress4[domain]
} else {
cachedAddress, cached = c.saveAddress6[domain]
}
c.saveAccess.RUnlock()
if cached {
return cachedAddress, true
}
var address netip.Addr
_ = c.DB.View(func(tx *bbolt.Tx) error {
var bucket *bbolt.Bucket
if isIPv6 {
bucket = tx.Bucket(bucketFakeIPDomain6)
} else {
bucket = tx.Bucket(bucketFakeIPDomain4)
}
if bucket == nil {
return nil
}
address = M.AddrFromIP(bucket.Get([]byte(domain)))
return nil
})
return address, address.IsValid()
}
func (c *CacheFile) FakeIPReset() error { func (c *CacheFile) FakeIPReset() error {
return c.DB.Batch(func(tx *bbolt.Tx) error { return c.DB.Batch(func(tx *bbolt.Tx) error {
err := tx.DeleteBucket(bucketFakeIP) return tx.DeleteBucket(bucketFakeIP)
if err != nil {
return err
}
err = tx.DeleteBucket(bucketFakeIPDomain4)
if err != nil {
return err
}
return tx.DeleteBucket(bucketFakeIPDomain6)
}) })
} }

View File

@@ -7,15 +7,6 @@ type Map[K comparable, V any] struct {
m sync.Map m sync.Map
} }
func (m *Map[K, V]) Len() int {
var count int
m.m.Range(func(key, value any) bool {
count++
return true
})
return count
}
func (m *Map[K, V]) Load(key K) (V, bool) { func (m *Map[K, V]) Load(key K) (V, bool) {
v, ok := m.m.Load(key) v, ok := m.m.Load(key)
if !ok { if !ok {

View File

@@ -6,7 +6,6 @@ import (
"strconv" "strconv"
"time" "time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/json" "github.com/sagernet/sing-box/common/json"
"github.com/sagernet/sing-box/experimental/clashapi/trafficontrol" "github.com/sagernet/sing-box/experimental/clashapi/trafficontrol"
"github.com/sagernet/websocket" "github.com/sagernet/websocket"
@@ -15,10 +14,10 @@ import (
"github.com/go-chi/render" "github.com/go-chi/render"
) )
func connectionRouter(router adapter.Router, trafficManager *trafficontrol.Manager) http.Handler { func connectionRouter(trafficManager *trafficontrol.Manager) http.Handler {
r := chi.NewRouter() r := chi.NewRouter()
r.Get("/", getConnections(trafficManager)) r.Get("/", getConnections(trafficManager))
r.Delete("/", closeAllConnections(router, trafficManager)) r.Delete("/", closeAllConnections(trafficManager))
r.Delete("/{id}", closeConnection(trafficManager)) r.Delete("/{id}", closeConnection(trafficManager))
return r return r
} }
@@ -87,13 +86,12 @@ func closeConnection(trafficManager *trafficontrol.Manager) func(w http.Response
} }
} }
func closeAllConnections(router adapter.Router, trafficManager *trafficontrol.Manager) func(w http.ResponseWriter, r *http.Request) { func closeAllConnections(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) {
snapshot := trafficManager.Snapshot() snapshot := trafficManager.Snapshot()
for _, c := range snapshot.Connections { for _, c := range snapshot.Connections {
c.Close() c.Close()
} }
router.ResetNetwork()
render.NoContent(w, r) render.NoContent(w, r)
} }
} }

View File

@@ -6,7 +6,6 @@ import (
"net/http" "net/http"
"sort" "sort"
"strconv" "strconv"
"strings"
"time" "time"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
@@ -134,7 +133,7 @@ func getProxies(server *Server, router adapter.Router) func(w http.ResponseWrite
defaultTag = allProxies[0] defaultTag = allProxies[0]
} }
sort.SliceStable(allProxies, func(i, j int) bool { sort.Slice(allProxies, func(i, j int) bool {
return allProxies[i] == defaultTag return allProxies[i] == defaultTag
}) })
@@ -215,9 +214,6 @@ func getProxyDelay(server *Server) func(w http.ResponseWriter, r *http.Request)
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query() query := r.URL.Query()
url := query.Get("url") url := query.Get("url")
if strings.HasPrefix(url, "http://") {
url = ""
}
timeout, err := strconv.ParseInt(query.Get("timeout"), 10, 16) timeout, err := strconv.ParseInt(query.Get("timeout"), 10, 16)
if err != nil { if err != nil {
render.Status(r, http.StatusBadRequest) render.Status(r, http.StatusBadRequest)

View File

@@ -23,8 +23,6 @@ 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"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/service"
"github.com/sagernet/sing/service/filemanager"
"github.com/sagernet/websocket" "github.com/sagernet/websocket"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
@@ -39,7 +37,6 @@ func init() {
var _ adapter.ClashServer = (*Server)(nil) var _ adapter.ClashServer = (*Server)(nil)
type Server struct { type Server struct {
ctx context.Context
router adapter.Router router adapter.Router
logger log.Logger logger log.Logger
httpServer *http.Server httpServer *http.Server
@@ -49,7 +46,6 @@ type Server struct {
storeSelected bool storeSelected bool
storeFakeIP bool storeFakeIP bool
cacheFilePath string cacheFilePath string
cacheID string
cacheFile adapter.ClashCacheFile cacheFile adapter.ClashCacheFile
externalUI string externalUI string
@@ -57,11 +53,10 @@ type Server struct {
externalUIDownloadDetour string externalUIDownloadDetour string
} }
func NewServer(ctx context.Context, router adapter.Router, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.ClashServer, error) { func NewServer(router adapter.Router, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.ClashServer, error) {
trafficManager := trafficontrol.NewManager() trafficManager := trafficontrol.NewManager()
chiRouter := chi.NewRouter() chiRouter := chi.NewRouter()
server := &Server{ server := &Server{
ctx: ctx,
router: router, router: router,
logger: logFactory.NewLogger("clash-api"), logger: logFactory.NewLogger("clash-api"),
httpServer: &http.Server{ httpServer: &http.Server{
@@ -69,16 +64,13 @@ func NewServer(ctx context.Context, router adapter.Router, logFactory log.Observ
Handler: chiRouter, Handler: chiRouter,
}, },
trafficManager: trafficManager, trafficManager: trafficManager,
urlTestHistory: urltest.NewHistoryStorage(),
mode: strings.ToLower(options.DefaultMode), mode: strings.ToLower(options.DefaultMode),
storeSelected: options.StoreSelected, storeSelected: options.StoreSelected,
storeFakeIP: options.StoreFakeIP, storeFakeIP: options.StoreFakeIP,
externalUIDownloadURL: options.ExternalUIDownloadURL, externalUIDownloadURL: options.ExternalUIDownloadURL,
externalUIDownloadDetour: options.ExternalUIDownloadDetour, externalUIDownloadDetour: options.ExternalUIDownloadDetour,
} }
server.urlTestHistory = service.PtrFromContext[urltest.HistoryStorage](ctx)
if server.urlTestHistory == nil {
server.urlTestHistory = urltest.NewHistoryStorage()
}
if server.mode == "" { if server.mode == "" {
server.mode = "rule" server.mode = "rule"
} }
@@ -90,10 +82,9 @@ func NewServer(ctx context.Context, router adapter.Router, logFactory log.Observ
if foundPath, loaded := C.FindPath(cachePath); loaded { if foundPath, loaded := C.FindPath(cachePath); loaded {
cachePath = foundPath cachePath = foundPath
} else { } else {
cachePath = filemanager.BasePath(ctx, cachePath) cachePath = C.BasePath(cachePath)
} }
server.cacheFilePath = cachePath server.cacheFilePath = cachePath
server.cacheID = options.CacheID
} }
cors := cors.New(cors.Options{ cors := cors.New(cors.Options{
AllowedOrigins: []string{"*"}, AllowedOrigins: []string{"*"},
@@ -111,18 +102,16 @@ func NewServer(ctx context.Context, router adapter.Router, logFactory log.Observ
r.Mount("/configs", configRouter(server, logFactory, server.logger)) r.Mount("/configs", configRouter(server, logFactory, server.logger))
r.Mount("/proxies", proxyRouter(server, router)) r.Mount("/proxies", proxyRouter(server, router))
r.Mount("/rules", ruleRouter(router)) r.Mount("/rules", ruleRouter(router))
r.Mount("/connections", connectionRouter(router, trafficManager)) r.Mount("/connections", connectionRouter(trafficManager))
r.Mount("/providers/proxies", proxyProviderRouter()) r.Mount("/providers/proxies", proxyProviderRouter())
r.Mount("/providers/rules", ruleProviderRouter()) r.Mount("/providers/rules", ruleProviderRouter())
r.Mount("/script", scriptRouter()) r.Mount("/script", scriptRouter())
r.Mount("/profile", profileRouter()) r.Mount("/profile", profileRouter())
r.Mount("/cache", cacheRouter(router)) r.Mount("/cache", cacheRouter(router))
r.Mount("/dns", dnsRouter(router)) r.Mount("/dns", dnsRouter(router))
server.setupMetaAPI(r)
}) })
if options.ExternalUI != "" { if options.ExternalUI != "" {
server.externalUI = filemanager.BasePath(ctx, os.ExpandEnv(options.ExternalUI)) server.externalUI = C.BasePath(os.ExpandEnv(options.ExternalUI))
chiRouter.Group(func(r chi.Router) { chiRouter.Group(func(r chi.Router) {
fs := http.StripPrefix("/ui", http.FileServer(http.Dir(server.externalUI))) fs := http.StripPrefix("/ui", http.FileServer(http.Dir(server.externalUI)))
r.Get("/ui", http.RedirectHandler("/ui/", http.StatusTemporaryRedirect).ServeHTTP) r.Get("/ui", http.RedirectHandler("/ui/", http.StatusTemporaryRedirect).ServeHTTP)
@@ -136,7 +125,7 @@ func NewServer(ctx context.Context, router adapter.Router, logFactory log.Observ
func (s *Server) PreStart() error { func (s *Server) PreStart() error {
if s.cacheFilePath != "" { if s.cacheFilePath != "" {
cacheFile, err := cachefile.Open(s.cacheFilePath, s.cacheID) cacheFile, err := cachefile.Open(s.cacheFilePath)
if err != nil { if err != nil {
return E.Cause(err, "open cache file") return E.Cause(err, "open cache file")
} }
@@ -189,10 +178,6 @@ func (s *Server) HistoryStorage() *urltest.HistoryStorage {
return s.urlTestHistory return s.urlTestHistory
} }
func (s *Server) TrafficManager() *trafficontrol.Manager {
return s.trafficManager
}
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, castMetadata(metadata), s.router, matchedRule)
return tracker, tracker return tracker, tracker
@@ -421,5 +406,5 @@ func getLogs(logFactory log.ObservableFactory) func(w http.ResponseWriter, r *ht
} }
func version(w http.ResponseWriter, r *http.Request) { func version(w http.ResponseWriter, r *http.Request) {
render.JSON(w, r, render.M{"version": "sing-box " + C.Version, "premium": true, "meta": true}) render.JSON(w, r, render.M{"version": "sing-box " + C.Version, "premium": true})
} }

View File

@@ -12,11 +12,11 @@ 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/common" "github.com/sagernet/sing/common"
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/service/filemanager"
) )
func (s *Server) checkAndDownloadExternalUI() { func (s *Server) checkAndDownloadExternalUI() {
@@ -40,7 +40,7 @@ func (s *Server) downloadExternalUI() error {
if s.externalUIDownloadURL != "" { if s.externalUIDownloadURL != "" {
downloadURL = s.externalUIDownloadURL downloadURL = s.externalUIDownloadURL
} else { } else {
downloadURL = "https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip" downloadURL = "https://github.com/Dreamacro/clash-dashboard/archive/refs/heads/gh-pages.zip"
} }
s.logger.Info("downloading external ui") s.logger.Info("downloading external ui")
var detour adapter.Outbound var detour adapter.Outbound
@@ -79,7 +79,7 @@ func (s *Server) downloadExternalUI() error {
} }
func (s *Server) downloadZIP(name string, body io.Reader, output string) error { func (s *Server) downloadZIP(name string, body io.Reader, output string) error {
tempFile, err := filemanager.CreateTemp(s.ctx, name) tempFile, err := C.CreateTemp(name)
if err != nil { if err != nil {
return err return err
} }
@@ -112,7 +112,7 @@ func (s *Server) downloadZIP(name string, body io.Reader, output string) error {
return err return err
} }
savePath := filepath.Join(saveDirectory, pathElements[len(pathElements)-1]) savePath := filepath.Join(saveDirectory, pathElements[len(pathElements)-1])
err = downloadZIPEntry(s.ctx, file, savePath) err = downloadZIPEntry(file, savePath)
if err != nil { if err != nil {
return err return err
} }
@@ -120,8 +120,8 @@ func (s *Server) downloadZIP(name string, body io.Reader, output string) error {
return nil return nil
} }
func downloadZIPEntry(ctx context.Context, zipFile *zip.File, savePath string) error { func downloadZIPEntry(zipFile *zip.File, savePath string) error {
saveFile, err := filemanager.Create(ctx, savePath) saveFile, err := os.Create(savePath)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -1,7 +1,6 @@
package trafficontrol package trafficontrol
import ( import (
"runtime"
"time" "time"
"github.com/sagernet/sing-box/experimental/clashapi/compatible" "github.com/sagernet/sing-box/experimental/clashapi/compatible"
@@ -19,15 +18,12 @@ type Manager struct {
connections compatible.Map[string, tracker] connections compatible.Map[string, tracker]
ticker *time.Ticker ticker *time.Ticker
done chan struct{} done chan struct{}
// process *process.Process
memory uint64
} }
func NewManager() *Manager { func NewManager() *Manager {
manager := &Manager{ manager := &Manager{
ticker: time.NewTicker(time.Second), ticker: time.NewTicker(time.Second),
done: make(chan struct{}), done: make(chan struct{}),
// process: &process.Process{Pid: int32(os.Getpid())},
} }
go manager.handle() go manager.handle()
return manager return manager
@@ -55,14 +51,6 @@ func (m *Manager) Now() (up int64, down int64) {
return m.uploadBlip.Load(), m.downloadBlip.Load() return m.uploadBlip.Load(), m.downloadBlip.Load()
} }
func (m *Manager) Total() (up int64, down int64) {
return m.uploadTotal.Load(), m.downloadTotal.Load()
}
func (m *Manager) Connections() int {
return m.connections.Len()
}
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(_ string, value tracker) bool {
@@ -70,18 +58,10 @@ func (m *Manager) Snapshot() *Snapshot {
return true return true
}) })
//if memoryInfo, err := m.process.MemoryInfo(); err == nil {
// m.memory = memoryInfo.RSS
//} else {
var memStats runtime.MemStats
runtime.ReadMemStats(&memStats)
m.memory = memStats.StackInuse + memStats.HeapInuse + memStats.HeapIdle - memStats.HeapReleased
return &Snapshot{ return &Snapshot{
UploadTotal: m.uploadTotal.Load(), UploadTotal: m.uploadTotal.Load(),
DownloadTotal: m.downloadTotal.Load(), DownloadTotal: m.downloadTotal.Load(),
Connections: connections, Connections: connections,
Memory: m.memory,
} }
} }
@@ -120,5 +100,4 @@ type Snapshot struct {
DownloadTotal int64 `json:"downloadTotal"` DownloadTotal int64 `json:"downloadTotal"`
UploadTotal int64 `json:"uploadTotal"` UploadTotal int64 `json:"uploadTotal"`
Connections []tracker `json:"connections"` Connections []tracker `json:"connections"`
Memory uint64 `json:"memory"`
} }

View File

@@ -7,12 +7,12 @@ import (
"time" "time"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/experimental/trackerconn"
"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"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
"github.com/gofrs/uuid/v5" "github.com/gofrs/uuid"
) )
type Metadata struct { type Metadata struct {
@@ -115,13 +115,13 @@ func NewTCPTracker(conn net.Conn, manager *Manager, metadata Metadata, router ad
download := new(atomic.Int64) download := new(atomic.Int64)
t := &tcpTracker{ t := &tcpTracker{
ExtendedConn: bufio.NewCounterConn(conn, []N.CountFunc{func(n int64) { ExtendedConn: trackerconn.NewHook(conn, func(n int64) {
upload.Add(n) upload.Add(n)
manager.PushUploaded(n) manager.PushUploaded(n)
}}, []N.CountFunc{func(n int64) { }, func(n int64) {
download.Add(n) download.Add(n)
manager.PushDownloaded(n) manager.PushDownloaded(n)
}}), }),
manager: manager, manager: manager,
trackerInfo: &trackerInfo{ trackerInfo: &trackerInfo{
UUID: uuid, UUID: uuid,
@@ -202,13 +202,13 @@ func NewUDPTracker(conn N.PacketConn, manager *Manager, metadata Metadata, route
download := new(atomic.Int64) download := new(atomic.Int64)
ut := &udpTracker{ ut := &udpTracker{
PacketConn: bufio.NewCounterPacketConn(conn, []N.CountFunc{func(n int64) { PacketConn: trackerconn.NewHookPacket(conn, func(n int64) {
upload.Add(n) upload.Add(n)
manager.PushUploaded(n) manager.PushUploaded(n)
}}, []N.CountFunc{func(n int64) { }, func(n int64) {
download.Add(n) download.Add(n)
manager.PushDownloaded(n) manager.PushDownloaded(n)
}}), }),
manager: manager, manager: manager,
trackerInfo: &trackerInfo{ trackerInfo: &trackerInfo{
UUID: uuid, UUID: uuid,

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