mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-06-29 13:22:06 +03:00
Compare commits
22 Commits
v1.4.0-bet
...
v1.3-beta9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bec606ee88 | ||
|
|
8545e41b2f | ||
|
|
d8810b6e8f | ||
|
|
f568bb9fe3 | ||
|
|
ccb872a41e | ||
|
|
9c287094e2 | ||
|
|
f61c5600e0 | ||
|
|
2e98777f82 | ||
|
|
73e72e9aec | ||
|
|
922acced94 | ||
|
|
988d7331c6 | ||
|
|
c0d6dde95b | ||
|
|
4dbf95875b | ||
|
|
81c4312be8 | ||
|
|
cc94dfaa4b | ||
|
|
c6fc411164 | ||
|
|
be00e19162 | ||
|
|
eb57cbc4ad | ||
|
|
f98cfdf5e4 | ||
|
|
cbf0099681 | ||
|
|
a86afa0e5b | ||
|
|
b5d2062359 |
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
12
Makefile
12
Makefile
@@ -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
|
||||||
@@ -89,8 +89,8 @@ lib:
|
|||||||
|
|
||||||
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-20230413023804-244d7ff07035
|
||||||
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-20230413023804-244d7ff07035
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf bin dist sing-box
|
rm -rf bin dist sing-box
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ type Tracker interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type OutboundGroup interface {
|
type OutboundGroup interface {
|
||||||
Outbound
|
|
||||||
Now() string
|
Now() string
|
||||||
All() []string
|
All() []string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -18,15 +17,7 @@ type FakeIPStore interface {
|
|||||||
type FakeIPStorage interface {
|
type FakeIPStorage interface {
|
||||||
FakeIPMetadata() *FakeIPMetadata
|
FakeIPMetadata() *FakeIPMetadata
|
||||||
FakeIPSaveMetadata(metadata *FakeIPMetadata) error
|
FakeIPSaveMetadata(metadata *FakeIPMetadata) error
|
||||||
FakeIPSaveMetadataAsync(metadata *FakeIPMetadata)
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -42,7 +45,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 +56,6 @@ type Router interface {
|
|||||||
|
|
||||||
V2RayServer() V2RayServer
|
V2RayServer() V2RayServer
|
||||||
SetV2RayServer(server V2RayServer)
|
SetV2RayServer(server V2RayServer)
|
||||||
|
|
||||||
ResetNetwork() error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type routerContextKey struct{}
|
type routerContextKey struct{}
|
||||||
@@ -84,6 +87,11 @@ type DNSRule interface {
|
|||||||
RewriteTTL() *uint32
|
RewriteTTL() *uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
type InterfaceUpdateListener interface {
|
type IPRule interface {
|
||||||
InterfaceUpdated()
|
Rule
|
||||||
|
Action() tun.ActionType
|
||||||
|
}
|
||||||
|
|
||||||
|
type InterfaceUpdateListener interface {
|
||||||
|
InterfaceUpdated() error
|
||||||
}
|
}
|
||||||
|
|||||||
46
box.go
46
box.go
@@ -19,7 +19,6 @@ import (
|
|||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
F "github.com/sagernet/sing/common/format"
|
F "github.com/sagernet/sing/common/format"
|
||||||
"github.com/sagernet/sing/service/pause"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ adapter.Service = (*Box)(nil)
|
var _ adapter.Service = (*Box)(nil)
|
||||||
@@ -47,13 +46,12 @@ func New(options Options) (*Box, error) {
|
|||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
ctx = context.Background()
|
ctx = context.Background()
|
||||||
}
|
}
|
||||||
ctx = pause.ContextWithDefaultManager(ctx)
|
|
||||||
createdAt := time.Now()
|
createdAt := time.Now()
|
||||||
experimentalOptions := common.PtrValueOrDefault(options.Experimental)
|
experimentalOptions := common.PtrValueOrDefault(options.Experimental)
|
||||||
applyDebugOptions(common.PtrValueOrDefault(experimentalOptions.Debug))
|
applyDebugOptions(common.PtrValueOrDefault(experimentalOptions.Debug))
|
||||||
var needClashAPI bool
|
var needClashAPI bool
|
||||||
var needV2RayAPI bool
|
var needV2RayAPI bool
|
||||||
if experimentalOptions.ClashAPI != nil || options.PlatformInterface != nil {
|
if experimentalOptions.ClashAPI != nil && experimentalOptions.ClashAPI.ExternalController != "" {
|
||||||
needClashAPI = true
|
needClashAPI = true
|
||||||
}
|
}
|
||||||
if experimentalOptions.V2RayAPI != nil && experimentalOptions.V2RayAPI.Listen != "" {
|
if experimentalOptions.V2RayAPI != nil && experimentalOptions.V2RayAPI.Listen != "" {
|
||||||
@@ -213,17 +211,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()
|
||||||
}
|
}
|
||||||
@@ -253,26 +260,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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
package build_shared
|
package build_shared
|
||||||
|
|
||||||
import (
|
import "github.com/sagernet/sing/common/shell"
|
||||||
"github.com/sagernet/sing-box/common/badversion"
|
|
||||||
"github.com/sagernet/sing/common/shell"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ReadTag() (string, error) {
|
func ReadTag() (string, error) {
|
||||||
currentTag, err := shell.Exec("git", "describe", "--tags").ReadOutput()
|
currentTag, err := shell.Exec("git", "describe", "--tags").ReadOutput()
|
||||||
@@ -15,9 +12,5 @@ func ReadTag() (string, error) {
|
|||||||
return currentTag[1:], nil
|
return currentTag[1:], nil
|
||||||
}
|
}
|
||||||
shortCommit, _ := shell.Exec("git", "rev-parse", "--short", "HEAD").ReadOutput()
|
shortCommit, _ := shell.Exec("git", "rev-parse", "--short", "HEAD").ReadOutput()
|
||||||
version := badversion.Parse(currentTagRev[1:])
|
return currentTagRev[1:] + "-" + shortCommit, nil
|
||||||
if version.PreReleaseIdentifier == "" {
|
|
||||||
version.Patch++
|
|
||||||
}
|
|
||||||
return version.String() + "-" + shortCommit, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ type Version struct {
|
|||||||
Major int
|
Major int
|
||||||
Minor int
|
Minor int
|
||||||
Patch int
|
Patch int
|
||||||
Commit string
|
|
||||||
PreReleaseIdentifier string
|
PreReleaseIdentifier string
|
||||||
PreReleaseVersion int
|
PreReleaseVersion int
|
||||||
}
|
}
|
||||||
@@ -38,21 +37,16 @@ func (v Version) After(anotherVersion Version) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if v.PreReleaseIdentifier != "" && anotherVersion.PreReleaseIdentifier != "" {
|
if v.PreReleaseIdentifier != "" && anotherVersion.PreReleaseIdentifier != "" {
|
||||||
if v.PreReleaseIdentifier == anotherVersion.PreReleaseIdentifier {
|
if v.PreReleaseIdentifier == "beta" && anotherVersion.PreReleaseIdentifier == "alpha" {
|
||||||
if v.PreReleaseVersion > anotherVersion.PreReleaseVersion {
|
|
||||||
return true
|
|
||||||
} else if v.PreReleaseVersion < anotherVersion.PreReleaseVersion {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
} else if v.PreReleaseIdentifier == "rc" && anotherVersion.PreReleaseIdentifier == "beta" {
|
|
||||||
return true
|
|
||||||
} else if v.PreReleaseIdentifier == "beta" && anotherVersion.PreReleaseIdentifier == "rc" {
|
|
||||||
return false
|
|
||||||
} else if v.PreReleaseIdentifier == "beta" && anotherVersion.PreReleaseIdentifier == "alpha" {
|
|
||||||
return true
|
return true
|
||||||
} else if v.PreReleaseIdentifier == "alpha" && anotherVersion.PreReleaseIdentifier == "beta" {
|
} else if v.PreReleaseIdentifier == "alpha" && anotherVersion.PreReleaseIdentifier == "beta" {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if v.PreReleaseVersion > anotherVersion.PreReleaseVersion {
|
||||||
|
return true
|
||||||
|
} else if v.PreReleaseVersion < anotherVersion.PreReleaseVersion {
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -101,7 +95,7 @@ func Parse(versionName string) (version Version) {
|
|||||||
version.PreReleaseIdentifier = "beta"
|
version.PreReleaseIdentifier = "beta"
|
||||||
version.PreReleaseVersion, _ = strconv.Atoi(identifier[4:])
|
version.PreReleaseVersion, _ = strconv.Atoi(identifier[4:])
|
||||||
} else {
|
} else {
|
||||||
version.Commit = identifier
|
version.PreReleaseIdentifier = identifier
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
87
common/debugio/log.go
Normal file
87
common/debugio/log.go
Normal 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
19
common/debugio/print.go
Normal 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
48
common/debugio/race.go
Normal 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
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ type DefaultDialer struct {
|
|||||||
udpAddr6 string
|
udpAddr6 string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDialer, error) {
|
func NewDefault(router adapter.Router, options option.DialerOptions) *DefaultDialer {
|
||||||
var dialer net.Dialer
|
var dialer net.Dialer
|
||||||
var listener net.ListenConfig
|
var listener net.ListenConfig
|
||||||
if options.BindInterface != "" {
|
if options.BindInterface != "" {
|
||||||
@@ -93,12 +93,6 @@ func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDi
|
|||||||
udpDialer6.LocalAddr = &net.UDPAddr{IP: bindAddr.AsSlice()}
|
udpDialer6.LocalAddr = &net.UDPAddr{IP: bindAddr.AsSlice()}
|
||||||
udpAddr6 = M.SocksaddrFrom(bindAddr, 0).String()
|
udpAddr6 = M.SocksaddrFrom(bindAddr, 0).String()
|
||||||
}
|
}
|
||||||
if options.TCPMultiPath {
|
|
||||||
if !multipathTCPAvailable {
|
|
||||||
return nil, E.New("MultiPath TCP requires go1.21, please recompile your binary.")
|
|
||||||
}
|
|
||||||
setMultiPathTCP(&dialer4)
|
|
||||||
}
|
|
||||||
return &DefaultDialer{
|
return &DefaultDialer{
|
||||||
tfo.Dialer{Dialer: dialer4, DisableTFO: !options.TCPFastOpen},
|
tfo.Dialer{Dialer: dialer4, DisableTFO: !options.TCPFastOpen},
|
||||||
tfo.Dialer{Dialer: dialer6, DisableTFO: !options.TCPFastOpen},
|
tfo.Dialer{Dialer: dialer6, DisableTFO: !options.TCPFastOpen},
|
||||||
@@ -107,7 +101,7 @@ func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDi
|
|||||||
listener,
|
listener,
|
||||||
udpAddr4,
|
udpAddr4,
|
||||||
udpAddr6,
|
udpAddr6,
|
||||||
}, nil
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DefaultDialer) DialContext(ctx context.Context, network string, address M.Socksaddr) (net.Conn, error) {
|
func (d *DefaultDialer) DialContext(ctx context.Context, network string, address M.Socksaddr) (net.Conn, error) {
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
//go:build go1.21
|
|
||||||
|
|
||||||
package dialer
|
|
||||||
|
|
||||||
import "net"
|
|
||||||
|
|
||||||
const multipathTCPAvailable = true
|
|
||||||
|
|
||||||
func setMultiPathTCP(dialer *net.Dialer) {
|
|
||||||
dialer.SetMultipathTCP(true)
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
//go:build !go1.21
|
|
||||||
|
|
||||||
package dialer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
)
|
|
||||||
|
|
||||||
const multipathTCPAvailable = false
|
|
||||||
|
|
||||||
func setMultiPathTCP(dialer *net.Dialer) {
|
|
||||||
}
|
|
||||||
@@ -6,24 +6,13 @@ import (
|
|||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
"github.com/sagernet/sing-dns"
|
"github.com/sagernet/sing-dns"
|
||||||
"github.com/sagernet/sing/common"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
)
|
)
|
||||||
|
|
||||||
func MustNew(router adapter.Router, options option.DialerOptions) N.Dialer {
|
func New(router adapter.Router, options option.DialerOptions) N.Dialer {
|
||||||
return common.Must1(New(router, options))
|
var dialer N.Dialer
|
||||||
}
|
|
||||||
|
|
||||||
func New(router adapter.Router, options option.DialerOptions) (N.Dialer, error) {
|
|
||||||
var (
|
|
||||||
dialer N.Dialer
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
if options.Detour == "" {
|
if options.Detour == "" {
|
||||||
dialer, err = NewDefault(router, options)
|
dialer = NewDefault(router, options)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
dialer = NewDetour(router, options.Detour)
|
dialer = NewDetour(router, options.Detour)
|
||||||
}
|
}
|
||||||
@@ -31,5 +20,5 @@ func New(router adapter.Router, options option.DialerOptions) (N.Dialer, error)
|
|||||||
if domainStrategy != dns.DomainStrategyAsIS || options.Detour == "" {
|
if domainStrategy != dns.DomainStrategyAsIS || options.Detour == "" {
|
||||||
dialer = NewResolveDialer(router, dialer, domainStrategy, time.Duration(options.FallbackDelay))
|
dialer = NewResolveDialer(router, dialer, domainStrategy, time.Duration(options.FallbackDelay))
|
||||||
}
|
}
|
||||||
return dialer, nil
|
return dialer
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -1,21 +1,554 @@
|
|||||||
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"
|
||||||
|
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
|
||||||
|
paddingEnabled bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClient(ctx context.Context, dialer N.Dialer, protocol Protocol, maxConnections int, minStreams int, maxStreams int, paddingEnabled bool) (*Client, error) {
|
||||||
|
return &Client{
|
||||||
|
ctx: ctx,
|
||||||
|
dialer: dialer,
|
||||||
|
protocol: protocol,
|
||||||
|
maxConnections: maxConnections,
|
||||||
|
minStreams: minStreams,
|
||||||
|
maxStreams: maxStreams,
|
||||||
|
paddingEnabled: paddingEnabled,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClientWithOptions(ctx context.Context, dialer N.Dialer, options option.MultiplexOptions) (*Client, 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, options.Padding)
|
||||||
|
}
|
||||||
|
|
||||||
|
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.NewUnbindPacketConn(&ClientPacketConn{ExtendedConn: bufio.NewExtendedConn(stream), 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 &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()
|
||||||
|
}
|
||||||
|
session := common.MinBy(common.Filter(sessions, abstractSession.CanTakeNewRequest), abstractSession.NumStreams)
|
||||||
|
if session == nil {
|
||||||
|
return c.offerNew()
|
||||||
|
}
|
||||||
|
numStreams := session.NumStreams()
|
||||||
|
if numStreams == 0 {
|
||||||
|
return session, nil
|
||||||
|
}
|
||||||
|
if c.maxConnections > 0 {
|
||||||
|
if len(sessions) >= 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
|
||||||
|
}
|
||||||
|
var version byte
|
||||||
|
if c.paddingEnabled {
|
||||||
|
version = Version1
|
||||||
|
} else {
|
||||||
|
version = Version0
|
||||||
|
}
|
||||||
|
conn = newProtocolConn(conn, Request{
|
||||||
|
Version: version,
|
||||||
|
Protocol: c.protocol,
|
||||||
|
PaddingEnabled: c.paddingEnabled,
|
||||||
|
})
|
||||||
|
if c.paddingEnabled {
|
||||||
|
conn = newPaddingConn(conn)
|
||||||
|
}
|
||||||
|
session, err := c.protocol.newClient(conn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
c.connections.PushBack(session)
|
||||||
|
return session, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Reset() {
|
||||||
|
c.access.Lock()
|
||||||
|
defer c.access.Unlock()
|
||||||
|
for _, session := range c.connections.Array() {
|
||||||
|
session.Close()
|
||||||
|
}
|
||||||
|
c.connections.Init()
|
||||||
|
}
|
||||||
|
|
||||||
|
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(streamRequestLen(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) NeedAdditionalReadDeadline() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
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 := streamRequestLen(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) NeedAdditionalReadDeadline() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
if destination.IsFqdn() {
|
||||||
|
addr = destination
|
||||||
|
} else {
|
||||||
|
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 := streamRequestLen(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) NeedAdditionalReadDeadline() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ClientPacketAddrConn) Upstream() any {
|
||||||
|
return c.ExtendedConn
|
||||||
}
|
}
|
||||||
|
|||||||
235
common/mux/h2mux.go
Normal file
235
common/mux/h2mux.go
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
package mux
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/transport/v2rayhttp"
|
||||||
|
"github.com/sagernet/sing/common/atomic"
|
||||||
|
"github.com/sagernet/sing/common/buf"
|
||||||
|
"github.com/sagernet/sing/common/bufio"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
|
||||||
|
"golang.org/x/net/http2"
|
||||||
|
)
|
||||||
|
|
||||||
|
const idleTimeout = 30 * time.Second
|
||||||
|
|
||||||
|
var _ abstractSession = (*H2MuxServerSession)(nil)
|
||||||
|
|
||||||
|
type H2MuxServerSession struct {
|
||||||
|
server http2.Server
|
||||||
|
active atomic.Int32
|
||||||
|
conn net.Conn
|
||||||
|
inbound chan net.Conn
|
||||||
|
done chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewH2MuxServer(conn net.Conn) *H2MuxServerSession {
|
||||||
|
session := &H2MuxServerSession{
|
||||||
|
conn: conn,
|
||||||
|
inbound: make(chan net.Conn),
|
||||||
|
done: make(chan struct{}),
|
||||||
|
server: http2.Server{
|
||||||
|
IdleTimeout: idleTimeout,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
session.server.ServeConn(conn, &http2.ServeConnOpts{
|
||||||
|
Handler: session,
|
||||||
|
})
|
||||||
|
_ = session.Close()
|
||||||
|
}()
|
||||||
|
return session
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *H2MuxServerSession) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
s.active.Add(1)
|
||||||
|
defer s.active.Add(-1)
|
||||||
|
writer.WriteHeader(http.StatusOK)
|
||||||
|
conn := newHTTP2Wrapper(&v2rayhttp.ServerHTTPConn{
|
||||||
|
HTTP2Conn: v2rayhttp.NewHTTPConn(request.Body, writer),
|
||||||
|
Flusher: writer.(http.Flusher),
|
||||||
|
})
|
||||||
|
s.inbound <- conn
|
||||||
|
select {
|
||||||
|
case <-conn.done:
|
||||||
|
case <-s.done:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *H2MuxServerSession) Open() (net.Conn, error) {
|
||||||
|
return nil, os.ErrInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *H2MuxServerSession) Accept() (net.Conn, error) {
|
||||||
|
select {
|
||||||
|
case conn := <-s.inbound:
|
||||||
|
return conn, nil
|
||||||
|
case <-s.done:
|
||||||
|
return nil, os.ErrClosed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *H2MuxServerSession) NumStreams() int {
|
||||||
|
return int(s.active.Load())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *H2MuxServerSession) Close() error {
|
||||||
|
select {
|
||||||
|
case <-s.done:
|
||||||
|
default:
|
||||||
|
close(s.done)
|
||||||
|
}
|
||||||
|
return s.conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *H2MuxServerSession) IsClosed() bool {
|
||||||
|
select {
|
||||||
|
case <-s.done:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *H2MuxServerSession) CanTakeNewRequest() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
type h2MuxConnWrapper struct {
|
||||||
|
N.ExtendedConn
|
||||||
|
done chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHTTP2Wrapper(conn net.Conn) *h2MuxConnWrapper {
|
||||||
|
return &h2MuxConnWrapper{
|
||||||
|
ExtendedConn: bufio.NewExtendedConn(conn),
|
||||||
|
done: make(chan struct{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *h2MuxConnWrapper) Write(p []byte) (n int, err error) {
|
||||||
|
select {
|
||||||
|
case <-w.done:
|
||||||
|
return 0, net.ErrClosed
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
return w.ExtendedConn.Write(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *h2MuxConnWrapper) WriteBuffer(buffer *buf.Buffer) error {
|
||||||
|
select {
|
||||||
|
case <-w.done:
|
||||||
|
return net.ErrClosed
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
return w.ExtendedConn.WriteBuffer(buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *h2MuxConnWrapper) Close() error {
|
||||||
|
select {
|
||||||
|
case <-w.done:
|
||||||
|
default:
|
||||||
|
close(w.done)
|
||||||
|
}
|
||||||
|
return w.ExtendedConn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *h2MuxConnWrapper) Upstream() any {
|
||||||
|
return w.ExtendedConn
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ abstractSession = (*H2MuxClientSession)(nil)
|
||||||
|
|
||||||
|
type H2MuxClientSession struct {
|
||||||
|
transport *http2.Transport
|
||||||
|
clientConn *http2.ClientConn
|
||||||
|
done chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewH2MuxClient(conn net.Conn) (*H2MuxClientSession, error) {
|
||||||
|
session := &H2MuxClientSession{
|
||||||
|
transport: &http2.Transport{
|
||||||
|
DialTLSContext: func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) {
|
||||||
|
return conn, nil
|
||||||
|
},
|
||||||
|
ReadIdleTimeout: idleTimeout,
|
||||||
|
},
|
||||||
|
done: make(chan struct{}),
|
||||||
|
}
|
||||||
|
session.transport.ConnPool = session
|
||||||
|
clientConn, err := session.transport.NewClientConn(conn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
session.clientConn = clientConn
|
||||||
|
return session, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *H2MuxClientSession) GetClientConn(req *http.Request, addr string) (*http2.ClientConn, error) {
|
||||||
|
return s.clientConn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *H2MuxClientSession) MarkDead(conn *http2.ClientConn) {
|
||||||
|
s.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *H2MuxClientSession) Open() (net.Conn, error) {
|
||||||
|
pipeInReader, pipeInWriter := io.Pipe()
|
||||||
|
request := &http.Request{
|
||||||
|
Method: http.MethodConnect,
|
||||||
|
Body: pipeInReader,
|
||||||
|
URL: &url.URL{Scheme: "https", Host: "localhost"},
|
||||||
|
}
|
||||||
|
conn := v2rayhttp.NewLateHTTPConn(pipeInWriter)
|
||||||
|
go func() {
|
||||||
|
response, err := s.transport.RoundTrip(request)
|
||||||
|
if err != nil {
|
||||||
|
conn.Setup(nil, err)
|
||||||
|
} else if response.StatusCode != 200 {
|
||||||
|
response.Body.Close()
|
||||||
|
conn.Setup(nil, E.New("unexpected status: ", response.StatusCode, " ", response.Status))
|
||||||
|
} else {
|
||||||
|
conn.Setup(response.Body, nil)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *H2MuxClientSession) Accept() (net.Conn, error) {
|
||||||
|
return nil, os.ErrInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *H2MuxClientSession) NumStreams() int {
|
||||||
|
return s.clientConn.State().StreamsActive
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *H2MuxClientSession) Close() error {
|
||||||
|
select {
|
||||||
|
case <-s.done:
|
||||||
|
default:
|
||||||
|
close(s.done)
|
||||||
|
}
|
||||||
|
return s.clientConn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *H2MuxClientSession) IsClosed() bool {
|
||||||
|
select {
|
||||||
|
case <-s.done:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
return s.clientConn.State().Closed
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *H2MuxClientSession) CanTakeNewRequest() bool {
|
||||||
|
return s.clientConn.CanTakeNewRequest()
|
||||||
|
}
|
||||||
240
common/mux/padding.go
Normal file
240
common/mux/padding.go
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
package mux
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"io"
|
||||||
|
"math/rand"
|
||||||
|
"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/sing/common/rw"
|
||||||
|
)
|
||||||
|
|
||||||
|
const kFirstPaddings = 16
|
||||||
|
|
||||||
|
type paddingConn struct {
|
||||||
|
N.ExtendedConn
|
||||||
|
writer N.VectorisedWriter
|
||||||
|
readPadding int
|
||||||
|
writePadding int
|
||||||
|
readRemaining int
|
||||||
|
paddingRemaining int
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPaddingConn(conn net.Conn) net.Conn {
|
||||||
|
writer, isVectorised := bufio.CreateVectorisedWriter(conn)
|
||||||
|
if isVectorised {
|
||||||
|
return &vectorisedPaddingConn{
|
||||||
|
paddingConn{
|
||||||
|
ExtendedConn: bufio.NewExtendedConn(conn),
|
||||||
|
writer: bufio.NewVectorisedWriter(conn),
|
||||||
|
},
|
||||||
|
writer,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return &paddingConn{
|
||||||
|
ExtendedConn: bufio.NewExtendedConn(conn),
|
||||||
|
writer: bufio.NewVectorisedWriter(conn),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *paddingConn) Read(p []byte) (n int, err error) {
|
||||||
|
if c.readRemaining > 0 {
|
||||||
|
if len(p) > c.readRemaining {
|
||||||
|
p = p[:c.readRemaining]
|
||||||
|
}
|
||||||
|
n, err = c.ExtendedConn.Read(p)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.readRemaining -= n
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if c.paddingRemaining > 0 {
|
||||||
|
err = rw.SkipN(c.ExtendedConn, c.paddingRemaining)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.paddingRemaining = 0
|
||||||
|
}
|
||||||
|
if c.readPadding < kFirstPaddings {
|
||||||
|
var paddingHdr []byte
|
||||||
|
if len(p) >= 4 {
|
||||||
|
paddingHdr = p[:4]
|
||||||
|
} else {
|
||||||
|
_paddingHdr := make([]byte, 4)
|
||||||
|
defer common.KeepAlive(_paddingHdr)
|
||||||
|
paddingHdr = common.Dup(_paddingHdr)
|
||||||
|
}
|
||||||
|
_, err = io.ReadFull(c.ExtendedConn, paddingHdr)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
originalDataSize := int(binary.BigEndian.Uint16(paddingHdr[:2]))
|
||||||
|
paddingLen := int(binary.BigEndian.Uint16(paddingHdr[2:]))
|
||||||
|
if len(p) > originalDataSize {
|
||||||
|
p = p[:originalDataSize]
|
||||||
|
}
|
||||||
|
n, err = c.ExtendedConn.Read(p)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.readPadding++
|
||||||
|
c.readRemaining = originalDataSize - n
|
||||||
|
c.paddingRemaining = paddingLen
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return c.ExtendedConn.Read(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *paddingConn) Write(p []byte) (n int, err error) {
|
||||||
|
for pLen := len(p); pLen > 0; {
|
||||||
|
var data []byte
|
||||||
|
if pLen > 65535 {
|
||||||
|
data = p[:65535]
|
||||||
|
p = p[65535:]
|
||||||
|
pLen -= 65535
|
||||||
|
} else {
|
||||||
|
data = p
|
||||||
|
pLen = 0
|
||||||
|
}
|
||||||
|
var writeN int
|
||||||
|
writeN, err = c.write(data)
|
||||||
|
n += writeN
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *paddingConn) write(p []byte) (n int, err error) {
|
||||||
|
if c.writePadding < kFirstPaddings {
|
||||||
|
paddingLen := 256 + rand.Intn(512)
|
||||||
|
_buffer := buf.StackNewSize(4 + len(p) + paddingLen)
|
||||||
|
defer common.KeepAlive(_buffer)
|
||||||
|
buffer := common.Dup(_buffer)
|
||||||
|
defer buffer.Release()
|
||||||
|
header := buffer.Extend(4)
|
||||||
|
binary.BigEndian.PutUint16(header[:2], uint16(len(p)))
|
||||||
|
binary.BigEndian.PutUint16(header[2:], uint16(paddingLen))
|
||||||
|
common.Must1(buffer.Write(p))
|
||||||
|
buffer.Extend(paddingLen)
|
||||||
|
_, err = c.ExtendedConn.Write(buffer.Bytes())
|
||||||
|
if err == nil {
|
||||||
|
n = len(p)
|
||||||
|
}
|
||||||
|
c.writePadding++
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return c.ExtendedConn.Write(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *paddingConn) ReadBuffer(buffer *buf.Buffer) error {
|
||||||
|
p := buffer.FreeBytes()
|
||||||
|
if c.readRemaining > 0 {
|
||||||
|
if len(p) > c.readRemaining {
|
||||||
|
p = p[:c.readRemaining]
|
||||||
|
}
|
||||||
|
n, err := c.ExtendedConn.Read(p)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.readRemaining -= n
|
||||||
|
buffer.Truncate(n)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if c.paddingRemaining > 0 {
|
||||||
|
err := rw.SkipN(c.ExtendedConn, c.paddingRemaining)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.paddingRemaining = 0
|
||||||
|
}
|
||||||
|
if c.readPadding < kFirstPaddings {
|
||||||
|
var paddingHdr []byte
|
||||||
|
if len(p) >= 4 {
|
||||||
|
paddingHdr = p[:4]
|
||||||
|
} else {
|
||||||
|
_paddingHdr := make([]byte, 4)
|
||||||
|
defer common.KeepAlive(_paddingHdr)
|
||||||
|
paddingHdr = common.Dup(_paddingHdr)
|
||||||
|
}
|
||||||
|
_, err := io.ReadFull(c.ExtendedConn, paddingHdr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
originalDataSize := int(binary.BigEndian.Uint16(paddingHdr[:2]))
|
||||||
|
paddingLen := int(binary.BigEndian.Uint16(paddingHdr[2:]))
|
||||||
|
|
||||||
|
if len(p) > originalDataSize {
|
||||||
|
p = p[:originalDataSize]
|
||||||
|
}
|
||||||
|
n, err := c.ExtendedConn.Read(p)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.readPadding++
|
||||||
|
c.readRemaining = originalDataSize - n
|
||||||
|
c.paddingRemaining = paddingLen
|
||||||
|
buffer.Truncate(n)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return c.ExtendedConn.ReadBuffer(buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *paddingConn) WriteBuffer(buffer *buf.Buffer) error {
|
||||||
|
if c.writePadding < kFirstPaddings {
|
||||||
|
bufferLen := buffer.Len()
|
||||||
|
if bufferLen > 65535 {
|
||||||
|
return common.Error(c.Write(buffer.Bytes()))
|
||||||
|
}
|
||||||
|
paddingLen := 256 + rand.Intn(512)
|
||||||
|
header := buffer.ExtendHeader(4)
|
||||||
|
binary.BigEndian.PutUint16(header[:2], uint16(bufferLen))
|
||||||
|
binary.BigEndian.PutUint16(header[2:], uint16(paddingLen))
|
||||||
|
buffer.Extend(paddingLen)
|
||||||
|
c.writePadding++
|
||||||
|
}
|
||||||
|
return c.ExtendedConn.WriteBuffer(buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *paddingConn) FrontHeadroom() int {
|
||||||
|
return 4 + 256 + 1024
|
||||||
|
}
|
||||||
|
|
||||||
|
type vectorisedPaddingConn struct {
|
||||||
|
paddingConn
|
||||||
|
writer N.VectorisedWriter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *vectorisedPaddingConn) WriteVectorised(buffers []*buf.Buffer) error {
|
||||||
|
if c.writePadding < kFirstPaddings {
|
||||||
|
bufferLen := buf.LenMulti(buffers)
|
||||||
|
if bufferLen > 65535 {
|
||||||
|
defer buf.ReleaseMulti(buffers)
|
||||||
|
for _, buffer := range buffers {
|
||||||
|
_, err := c.Write(buffer.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
paddingLen := 256 + rand.Intn(512)
|
||||||
|
header := buf.NewSize(4)
|
||||||
|
common.Must(
|
||||||
|
binary.Write(header, binary.BigEndian, uint16(bufferLen)),
|
||||||
|
binary.Write(header, binary.BigEndian, uint16(paddingLen)),
|
||||||
|
)
|
||||||
|
c.writePadding++
|
||||||
|
padding := buf.NewSize(paddingLen)
|
||||||
|
padding.Extend(paddingLen)
|
||||||
|
buffers = append(append([]*buf.Buffer{header}, buffers...), padding)
|
||||||
|
}
|
||||||
|
return c.writer.WriteVectorised(buffers)
|
||||||
|
}
|
||||||
@@ -1,14 +1,299 @@
|
|||||||
package mux
|
package mux
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/sagernet/sing-mux"
|
"encoding/binary"
|
||||||
|
"io"
|
||||||
|
"math/rand"
|
||||||
|
"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
|
||||||
|
ProtocolH2Mux
|
||||||
)
|
)
|
||||||
|
|
||||||
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
|
||||||
|
case "h2mux":
|
||||||
|
return ProtocolH2Mux, nil
|
||||||
|
default:
|
||||||
|
return ProtocolSMux, 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:
|
||||||
|
session, err := yamux.Server(conn, yaMuxConfig())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &yamuxSession{session}, nil
|
||||||
|
case ProtocolH2Mux:
|
||||||
|
return NewH2MuxServer(conn), nil
|
||||||
|
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:
|
||||||
|
session, err := yamux.Client(conn, yaMuxConfig())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &yamuxSession{session}, nil
|
||||||
|
case ProtocolH2Mux:
|
||||||
|
return NewH2MuxClient(conn)
|
||||||
|
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"
|
||||||
|
case ProtocolH2Mux:
|
||||||
|
return "h2mux"
|
||||||
|
default:
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
Version0 = iota
|
||||||
|
Version1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Request struct {
|
||||||
|
Version byte
|
||||||
|
Protocol Protocol
|
||||||
|
PaddingEnabled bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadRequest(reader io.Reader) (*Request, error) {
|
||||||
|
version, err := rw.ReadByte(reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if version < Version0 || version > Version1 {
|
||||||
|
return nil, E.New("unsupported version: ", version)
|
||||||
|
}
|
||||||
|
protocol, err := rw.ReadByte(reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var paddingEnabled bool
|
||||||
|
if version == Version1 {
|
||||||
|
err = binary.Read(reader, binary.BigEndian, &paddingEnabled)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if paddingEnabled {
|
||||||
|
var paddingLen uint16
|
||||||
|
err = binary.Read(reader, binary.BigEndian, &paddingLen)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = rw.SkipN(reader, int(paddingLen))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &Request{Version: version, Protocol: Protocol(protocol), PaddingEnabled: paddingEnabled}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func EncodeRequest(request Request, payload []byte) *buf.Buffer {
|
||||||
|
var requestLen int
|
||||||
|
requestLen += 2
|
||||||
|
var paddingLen uint16
|
||||||
|
if request.Version == Version1 {
|
||||||
|
requestLen += 1
|
||||||
|
if request.PaddingEnabled {
|
||||||
|
requestLen += 2
|
||||||
|
paddingLen = uint16(256 + rand.Intn(512))
|
||||||
|
requestLen += int(paddingLen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buffer := buf.NewSize(requestLen + len(payload))
|
||||||
|
common.Must(
|
||||||
|
buffer.WriteByte(request.Version),
|
||||||
|
buffer.WriteByte(byte(request.Protocol)),
|
||||||
|
)
|
||||||
|
if request.Version == Version1 {
|
||||||
|
common.Must(binary.Write(buffer, binary.BigEndian, request.PaddingEnabled))
|
||||||
|
if request.PaddingEnabled {
|
||||||
|
common.Must(binary.Write(buffer, binary.BigEndian, paddingLen))
|
||||||
|
buffer.Extend(int(paddingLen))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
common.Must1(buffer.Write(payload))
|
||||||
|
return buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
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 streamRequestLen(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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
272
common/mux/service.go
Normal file
272
common/mux/service.go
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
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"
|
||||||
|
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
|
||||||
|
}
|
||||||
|
if request.PaddingEnabled {
|
||||||
|
conn = newPaddingConn(conn)
|
||||||
|
}
|
||||||
|
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, 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) NeedAdditionalReadDeadline() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
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) NeedAdditionalReadDeadline() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
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) NeedAdditionalReadDeadline() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ServerPacketAddrConn) Upstream() any {
|
||||||
|
return c.ExtendedConn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ServerPacketAddrConn) FrontHeadroom() int {
|
||||||
|
if !c.responseWrite {
|
||||||
|
return 3 + M.MaxSocksaddrLength
|
||||||
|
}
|
||||||
|
return 2 + M.MaxSocksaddrLength
|
||||||
|
}
|
||||||
111
common/mux/session.go
Normal file
111
common/mux/session.go
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
package mux
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing/common/buf"
|
||||||
|
"github.com/sagernet/sing/common/bufio"
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
"github.com/sagernet/smux"
|
||||||
|
|
||||||
|
"github.com/hashicorp/yamux"
|
||||||
|
)
|
||||||
|
|
||||||
|
type abstractSession interface {
|
||||||
|
Open() (net.Conn, error)
|
||||||
|
Accept() (net.Conn, error)
|
||||||
|
NumStreams() int
|
||||||
|
Close() error
|
||||||
|
IsClosed() bool
|
||||||
|
CanTakeNewRequest() 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *smuxSession) CanTakeNewRequest() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
type yamuxSession struct {
|
||||||
|
*yamux.Session
|
||||||
|
}
|
||||||
|
|
||||||
|
func (y *yamuxSession) CanTakeNewRequest() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
type protocolConn struct {
|
||||||
|
net.Conn
|
||||||
|
request Request
|
||||||
|
protocolWritten bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newProtocolConn(conn net.Conn, request Request) net.Conn {
|
||||||
|
writer, isVectorised := bufio.CreateVectorisedWriter(conn)
|
||||||
|
if isVectorised {
|
||||||
|
return &vectorisedProtocolConn{
|
||||||
|
protocolConn{
|
||||||
|
Conn: conn,
|
||||||
|
request: request,
|
||||||
|
},
|
||||||
|
writer,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return &protocolConn{
|
||||||
|
Conn: conn,
|
||||||
|
request: request,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *protocolConn) Write(p []byte) (n int, err error) {
|
||||||
|
if c.protocolWritten {
|
||||||
|
return c.Conn.Write(p)
|
||||||
|
}
|
||||||
|
buffer := EncodeRequest(c.request, p)
|
||||||
|
n, err = c.Conn.Write(buffer.Bytes())
|
||||||
|
buffer.Release()
|
||||||
|
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
|
||||||
|
writer N.VectorisedWriter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *vectorisedProtocolConn) WriteVectorised(buffers []*buf.Buffer) error {
|
||||||
|
if c.protocolWritten {
|
||||||
|
return c.writer.WriteVectorised(buffers)
|
||||||
|
}
|
||||||
|
c.protocolWritten = true
|
||||||
|
buffer := EncodeRequest(c.request, nil)
|
||||||
|
return c.writer.WriteVectorised(append([]*buf.Buffer{buffer}, buffers...))
|
||||||
|
}
|
||||||
@@ -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())
|
||||||
|
|||||||
@@ -20,10 +20,10 @@ type systemProxy struct {
|
|||||||
isMixed bool
|
isMixed bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *systemProxy) update(event int) {
|
func (p *systemProxy) update(event int) error {
|
||||||
newInterfaceName := p.monitor.DefaultInterfaceName(netip.IPv4Unspecified())
|
newInterfaceName := p.monitor.DefaultInterfaceName(netip.IPv4Unspecified())
|
||||||
if p.interfaceName == newInterfaceName {
|
if p.interfaceName == newInterfaceName {
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
if p.interfaceName != "" {
|
if p.interfaceName != "" {
|
||||||
_ = p.unset()
|
_ = p.unset()
|
||||||
@@ -31,7 +31,7 @@ func (p *systemProxy) update(event int) {
|
|||||||
p.interfaceName = newInterfaceName
|
p.interfaceName = newInterfaceName
|
||||||
interfaceDisplayName, err := getInterfaceDisplayName(p.interfaceName)
|
interfaceDisplayName, err := getInterfaceDisplayName(p.interfaceName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
if p.isMixed {
|
if p.isMixed {
|
||||||
err = shell.Exec("networksetup", "-setsocksfirewallproxy", interfaceDisplayName, "127.0.0.1", F.ToString(p.port)).Attach().Run()
|
err = shell.Exec("networksetup", "-setsocksfirewallproxy", interfaceDisplayName, "127.0.0.1", F.ToString(p.port)).Attach().Run()
|
||||||
@@ -40,9 +40,9 @@ func (p *systemProxy) update(event int) {
|
|||||||
err = shell.Exec("networksetup", "-setwebproxy", interfaceDisplayName, "127.0.0.1", F.ToString(p.port)).Attach().Run()
|
err = shell.Exec("networksetup", "-setwebproxy", interfaceDisplayName, "127.0.0.1", F.ToString(p.port)).Attach().Run()
|
||||||
}
|
}
|
||||||
if err == nil {
|
if err == nil {
|
||||||
_ = shell.Exec("networksetup", "-setsecurewebproxy", interfaceDisplayName, "127.0.0.1", F.ToString(p.port)).Attach().Run()
|
err = shell.Exec("networksetup", "-setsecurewebproxy", interfaceDisplayName, "127.0.0.1", F.ToString(p.port)).Attach().Run()
|
||||||
}
|
}
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *systemProxy) unset() error {
|
func (p *systemProxy) unset() error {
|
||||||
@@ -88,7 +88,10 @@ func SetSystemProxy(router adapter.Router, port uint16, isMixed bool) (func() er
|
|||||||
port: port,
|
port: port,
|
||||||
isMixed: isMixed,
|
isMixed: isMixed,
|
||||||
}
|
}
|
||||||
proxy.update(tun.EventInterfaceUpdate)
|
err := proxy.update(tun.EventInterfaceUpdate)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
proxy.element = interfaceMonitor.RegisterCallback(proxy.update)
|
proxy.element = interfaceMonitor.RegisterCallback(proxy.update)
|
||||||
return func() error {
|
return func() error {
|
||||||
interfaceMonitor.UnregisterCallback(proxy.element)
|
interfaceMonitor.UnregisterCallback(proxy.element)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -101,10 +101,7 @@ func NewRealityServer(ctx context.Context, router adapter.Router, logger log.Log
|
|||||||
tlsConfig.ShortIds[shortID] = true
|
tlsConfig.ShortIds[shortID] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
handshakeDialer, err := dialer.New(router, options.Reality.Handshake.DialerOptions)
|
handshakeDialer := dialer.New(router, options.Reality.Handshake.DialerOptions)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
tlsConfig.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
tlsConfig.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
return handshakeDialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
|
return handshakeDialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -164,8 +164,8 @@ func NewSTDServer(ctx context.Context, router adapter.Router, logger log.Logger,
|
|||||||
var acmeService adapter.Service
|
var acmeService adapter.Service
|
||||||
var err error
|
var err error
|
||||||
if options.ACME != nil && len(options.ACME.Domain) > 0 {
|
if options.ACME != nil && len(options.ACME.Domain) > 0 {
|
||||||
//nolint:staticcheck
|
|
||||||
tlsConfig, acmeService, err = startACME(ctx, common.PtrValueOrDefault(options.ACME))
|
tlsConfig, acmeService, err = startACME(ctx, common.PtrValueOrDefault(options.ACME))
|
||||||
|
//nolint:staticcheck
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 != "" {
|
||||||
|
|||||||
@@ -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,24 +39,14 @@ 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) {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const (
|
|||||||
TypeDirect = "direct"
|
TypeDirect = "direct"
|
||||||
TypeBlock = "block"
|
TypeBlock = "block"
|
||||||
TypeDNS = "dns"
|
TypeDNS = "dns"
|
||||||
TypeSOCKS = "socks"
|
TypeSocks = "socks"
|
||||||
TypeHTTP = "http"
|
TypeHTTP = "http"
|
||||||
TypeMixed = "mixed"
|
TypeMixed = "mixed"
|
||||||
TypeShadowsocks = "shadowsocks"
|
TypeShadowsocks = "shadowsocks"
|
||||||
@@ -21,55 +21,9 @@ const (
|
|||||||
TypeShadowTLS = "shadowtls"
|
TypeShadowTLS = "shadowtls"
|
||||||
TypeShadowsocksR = "shadowsocksr"
|
TypeShadowsocksR = "shadowsocksr"
|
||||||
TypeVLESS = "vless"
|
TypeVLESS = "vless"
|
||||||
TypeTUIC = "tuic"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
TypeSelector = "selector"
|
TypeSelector = "selector"
|
||||||
TypeURLTest = "urltest"
|
TypeURLTest = "urltest"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ProxyDisplayName(proxyType string) string {
|
|
||||||
switch proxyType {
|
|
||||||
case TypeDirect:
|
|
||||||
return "Direct"
|
|
||||||
case TypeBlock:
|
|
||||||
return "Block"
|
|
||||||
case TypeDNS:
|
|
||||||
return "DNS"
|
|
||||||
case TypeSOCKS:
|
|
||||||
return "SOCKS"
|
|
||||||
case TypeHTTP:
|
|
||||||
return "HTTP"
|
|
||||||
case TypeShadowsocks:
|
|
||||||
return "Shadowsocks"
|
|
||||||
case TypeVMess:
|
|
||||||
return "VMess"
|
|
||||||
case TypeTrojan:
|
|
||||||
return "Trojan"
|
|
||||||
case TypeNaive:
|
|
||||||
return "Naive"
|
|
||||||
case TypeWireGuard:
|
|
||||||
return "WireGuard"
|
|
||||||
case TypeHysteria:
|
|
||||||
return "Hysteria"
|
|
||||||
case TypeTor:
|
|
||||||
return "Tor"
|
|
||||||
case TypeSSH:
|
|
||||||
return "SSH"
|
|
||||||
case TypeShadowTLS:
|
|
||||||
return "ShadowTLS"
|
|
||||||
case TypeShadowsocksR:
|
|
||||||
return "ShadowsocksR"
|
|
||||||
case TypeVLESS:
|
|
||||||
return "VLESS"
|
|
||||||
case TypeTUIC:
|
|
||||||
return "TUIC"
|
|
||||||
case TypeSelector:
|
|
||||||
return "Selector"
|
|
||||||
case TypeURLTest:
|
|
||||||
return "URLTest"
|
|
||||||
default:
|
|
||||||
return "Unknown"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,165 +1,3 @@
|
|||||||
#### 1.4.0-beta.2
|
|
||||||
|
|
||||||
* Add MultiPath TCP support **1**
|
|
||||||
* Drop QUIC support for Go 1.18 and 1.19 due to upstream changes
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
*1*:
|
|
||||||
|
|
||||||
Requires sing-box to be compiled with Go 1.21.
|
|
||||||
|
|
||||||
#### 1.4.0-beta.1
|
|
||||||
|
|
||||||
* Add TUIC support **1**
|
|
||||||
* Pause recurring tasks when no network or device idle
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
*1*:
|
|
||||||
|
|
||||||
See [TUIC inbound](/configuration/inbound/tuic)
|
|
||||||
and [TUIC outbound](/configuration/outbound/tuic)
|
|
||||||
|
|
||||||
#### 1.3.6
|
|
||||||
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
#### 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
|
#### 1.3-beta9
|
||||||
|
|
||||||
* Improve multiplex **1**
|
* Improve multiplex **1**
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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 地址的反向映射以为路由目的提供域名。
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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 ""
|
||||||
|
|
||||||
|
|||||||
@@ -7,14 +7,13 @@
|
|||||||
"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_url": "",
|
||||||
"external_ui_download_detour": "",
|
"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",
|
||||||
@@ -92,12 +91,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 ""
|
||||||
|
|||||||
@@ -7,14 +7,13 @@
|
|||||||
"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_url": "",
|
||||||
"external_ui_download_detour": "",
|
"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",
|
||||||
@@ -90,12 +89,6 @@ Clash 中的默认模式,默认使用 `rule`。
|
|||||||
|
|
||||||
缓存文件路径,默认使用`cache.db`。
|
缓存文件路径,默认使用`cache.db`。
|
||||||
|
|
||||||
#### cache_id
|
|
||||||
|
|
||||||
缓存 ID。
|
|
||||||
|
|
||||||
如果不为空,`store_selected` 将会使用以此为键的独立存储。
|
|
||||||
|
|
||||||
### V2Ray API 字段
|
### V2Ray API 字段
|
||||||
|
|
||||||
!!! error ""
|
!!! error ""
|
||||||
|
|||||||
@@ -1,82 +0,0 @@
|
|||||||
### Structure
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "tuic",
|
|
||||||
"tag": "tuic-in",
|
|
||||||
|
|
||||||
... // Listen Fields
|
|
||||||
|
|
||||||
"users": [
|
|
||||||
{
|
|
||||||
"name": "sekai",
|
|
||||||
"uuid": "059032A9-7D40-4A96-9BB1-36823D848068",
|
|
||||||
"password": "hello"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"congestion_control": "cubic",
|
|
||||||
"auth_timeout": "3s",
|
|
||||||
"zero_rtt_handshake": false,
|
|
||||||
"heartbeat": "10s",
|
|
||||||
"tls": {}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! warning ""
|
|
||||||
|
|
||||||
QUIC, which is required by TUIC is not included by default, see [Installation](/#installation).
|
|
||||||
|
|
||||||
### Listen Fields
|
|
||||||
|
|
||||||
See [Listen Fields](/configuration/shared/listen) for details.
|
|
||||||
|
|
||||||
### Fields
|
|
||||||
|
|
||||||
#### users
|
|
||||||
|
|
||||||
TUIC users
|
|
||||||
|
|
||||||
#### users.uuid
|
|
||||||
|
|
||||||
==Required==
|
|
||||||
|
|
||||||
TUIC user uuid
|
|
||||||
|
|
||||||
#### users.password
|
|
||||||
|
|
||||||
TUIC user password
|
|
||||||
|
|
||||||
#### congestion_control
|
|
||||||
|
|
||||||
QUIC congestion control algorithm
|
|
||||||
|
|
||||||
One of: `cubic`, `new_reno`, `bbr`
|
|
||||||
|
|
||||||
`cubic` is used by default.
|
|
||||||
|
|
||||||
#### auth_timeout
|
|
||||||
|
|
||||||
How long the server should wait for the client to send the authentication command
|
|
||||||
|
|
||||||
`3s` is used by default.
|
|
||||||
|
|
||||||
#### zero_rtt_handshake
|
|
||||||
|
|
||||||
Enable 0-RTT QUIC connection handshake on the client side
|
|
||||||
This is not impacting much on the performance, as the protocol is fully multiplexed
|
|
||||||
|
|
||||||
!!! warning ""
|
|
||||||
Disabling this is highly recommended, as it is vulnerable to replay attacks.
|
|
||||||
See [Attack of the clones](https://blog.cloudflare.com/even-faster-connection-establishment-with-quic-0-rtt-resumption/#attack-of-the-clones)
|
|
||||||
|
|
||||||
#### heartbeat
|
|
||||||
|
|
||||||
Interval for sending heartbeat packets for keeping the connection alive
|
|
||||||
|
|
||||||
`10s` is used by default.
|
|
||||||
|
|
||||||
#### tls
|
|
||||||
|
|
||||||
==Required==
|
|
||||||
|
|
||||||
TLS configuration, see [TLS](/configuration/shared/tls/#inbound).
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
### 结构
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "tuic",
|
|
||||||
"tag": "tuic-in",
|
|
||||||
|
|
||||||
... // 监听字段
|
|
||||||
|
|
||||||
"users": [
|
|
||||||
{
|
|
||||||
"name": "sekai",
|
|
||||||
"uuid": "059032A9-7D40-4A96-9BB1-36823D848068",
|
|
||||||
"password": "hello"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"congestion_control": "cubic",
|
|
||||||
"auth_timeout": "3s",
|
|
||||||
"zero_rtt_handshake": false,
|
|
||||||
"heartbeat": "10s",
|
|
||||||
"tls": {}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! warning ""
|
|
||||||
|
|
||||||
默认安装不包含被 TUI 依赖的 QUIC,参阅 [安装](/zh/#_2)。
|
|
||||||
|
|
||||||
### 监听字段
|
|
||||||
|
|
||||||
参阅 [监听字段](/zh/configuration/shared/listen/)。
|
|
||||||
|
|
||||||
### 字段
|
|
||||||
|
|
||||||
#### users
|
|
||||||
|
|
||||||
TUIC 用户
|
|
||||||
|
|
||||||
#### users.uuid
|
|
||||||
|
|
||||||
==必填==
|
|
||||||
|
|
||||||
TUIC 用户 UUID
|
|
||||||
|
|
||||||
#### users.password
|
|
||||||
|
|
||||||
TUIC 用户密码
|
|
||||||
|
|
||||||
#### congestion_control
|
|
||||||
|
|
||||||
QUIC 流量控制算法
|
|
||||||
|
|
||||||
可选值: `cubic`, `new_reno`, `bbr`
|
|
||||||
|
|
||||||
默认使用 `cubic`。
|
|
||||||
|
|
||||||
#### auth_timeout
|
|
||||||
|
|
||||||
服务器等待客户端发送认证命令的时间
|
|
||||||
|
|
||||||
默认使用 `3s`。
|
|
||||||
|
|
||||||
#### zero_rtt_handshake
|
|
||||||
|
|
||||||
在客户端启用 0-RTT QUIC 连接握手
|
|
||||||
这对性能影响不大,因为协议是完全复用的
|
|
||||||
|
|
||||||
!!! warning ""
|
|
||||||
强烈建议禁用此功能,因为它容易受到重放攻击。
|
|
||||||
请参阅 [Attack of the clones](https://blog.cloudflare.com/even-faster-connection-establishment-with-quic-0-rtt-resumption/#attack-of-the-clones)
|
|
||||||
|
|
||||||
#### heartbeat
|
|
||||||
|
|
||||||
发送心跳包以保持连接存活的时间间隔
|
|
||||||
|
|
||||||
默认使用 `10s`。
|
|
||||||
|
|
||||||
#### tls
|
|
||||||
|
|
||||||
==必填==
|
|
||||||
|
|
||||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
|
|
||||||
@@ -142,12 +142,11 @@ UDP NAT expiration time in seconds, default is 300 (5 minutes).
|
|||||||
|
|
||||||
TCP/IP stack.
|
TCP/IP stack.
|
||||||
|
|
||||||
| Stack | Description | Status |
|
| Stack | Description | Status |
|
||||||
|--------|----------------------------------------------------------------------------------|-------------------|
|
|------------------|----------------------------------------------------------------------------------|-------------------|
|
||||||
| system | Sometimes better performance | recommended |
|
| system (default) | Sometimes better performance | recommended |
|
||||||
| gVisor | Better compatibility, based on [google/gvisor](https://github.com/google/gvisor) | recommended |
|
| gVisor | Better compatibility, based on [google/gvisor](https://github.com/google/gvisor) | recommended |
|
||||||
| mixed | Mixed `system` TCP stack and `gVisor` UDP stack | recommended |
|
| LWIP | Based on [eycorsican/go-tun2socks](https://github.com/eycorsican/go-tun2socks) | upstream archived |
|
||||||
| LWIP | Based on [eycorsican/go-tun2socks](https://github.com/eycorsican/go-tun2socks) | upstream archived |
|
|
||||||
|
|
||||||
!!! warning ""
|
!!! warning ""
|
||||||
|
|
||||||
|
|||||||
@@ -97,6 +97,12 @@ Disables Path MTU Discovery (RFC 8899). Packets will then be at most 1252 (IPv4)
|
|||||||
|
|
||||||
Force enabled on for systems other than Linux and Windows (according to upstream).
|
Force enabled on for systems other than Linux and Windows (according to upstream).
|
||||||
|
|
||||||
|
#### tls
|
||||||
|
|
||||||
|
==Required==
|
||||||
|
|
||||||
|
TLS configuration, see [TLS](/configuration/shared/tls/#outbound).
|
||||||
|
|
||||||
#### network
|
#### network
|
||||||
|
|
||||||
Enabled network
|
Enabled network
|
||||||
@@ -105,12 +111,6 @@ One of `tcp` `udp`.
|
|||||||
|
|
||||||
Both is enabled by default.
|
Both is enabled by default.
|
||||||
|
|
||||||
#### tls
|
|
||||||
|
|
||||||
==Required==
|
|
||||||
|
|
||||||
TLS configuration, see [TLS](/configuration/shared/tls/#outbound).
|
|
||||||
|
|
||||||
### Dial Fields
|
### Dial Fields
|
||||||
|
|
||||||
See [Dial Fields](/configuration/shared/dial) for details.
|
See [Dial Fields](/configuration/shared/dial) for details.
|
||||||
|
|||||||
@@ -97,6 +97,10 @@ base64 编码的认证密码。
|
|||||||
|
|
||||||
强制为 Linux 和 Windows 以外的系统启用(根据上游)。
|
强制为 Linux 和 Windows 以外的系统启用(根据上游)。
|
||||||
|
|
||||||
|
==必填==
|
||||||
|
|
||||||
|
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
|
||||||
|
|
||||||
#### network
|
#### network
|
||||||
|
|
||||||
启用的网络协议。
|
启用的网络协议。
|
||||||
@@ -105,13 +109,6 @@ base64 编码的认证密码。
|
|||||||
|
|
||||||
默认所有。
|
默认所有。
|
||||||
|
|
||||||
#### tls
|
|
||||||
|
|
||||||
==必填==
|
|
||||||
|
|
||||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
|
|
||||||
|
|
||||||
|
|
||||||
### 拨号字段
|
### 拨号字段
|
||||||
|
|
||||||
参阅 [拨号字段](/zh/configuration/shared/dial/)。
|
参阅 [拨号字段](/zh/configuration/shared/dial/)。
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
### Structure
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "tuic",
|
|
||||||
"tag": "tuic-out",
|
|
||||||
|
|
||||||
"server": "127.0.0.1",
|
|
||||||
"server_port": 1080,
|
|
||||||
"uuid": "2DD61D93-75D8-4DA4-AC0E-6AECE7EAC365",
|
|
||||||
"password": "hello",
|
|
||||||
"congestion_control": "cubic",
|
|
||||||
"udp_relay_mode": "native",
|
|
||||||
"zero_rtt_handshake": false,
|
|
||||||
"heartbeat": "10s",
|
|
||||||
"network": "tcp",
|
|
||||||
"tls": {},
|
|
||||||
|
|
||||||
... // Dial Fields
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! warning ""
|
|
||||||
|
|
||||||
QUIC, which is required by TUIC is not included by default, see [Installation](/#installation).
|
|
||||||
|
|
||||||
### Fields
|
|
||||||
|
|
||||||
#### server
|
|
||||||
|
|
||||||
==Required==
|
|
||||||
|
|
||||||
The server address.
|
|
||||||
|
|
||||||
#### server_port
|
|
||||||
|
|
||||||
==Required==
|
|
||||||
|
|
||||||
The server port.
|
|
||||||
|
|
||||||
#### uuid
|
|
||||||
|
|
||||||
==Required==
|
|
||||||
|
|
||||||
TUIC user uuid
|
|
||||||
|
|
||||||
#### password
|
|
||||||
|
|
||||||
TUIC user password
|
|
||||||
|
|
||||||
#### congestion_control
|
|
||||||
|
|
||||||
QUIC congestion control algorithm
|
|
||||||
|
|
||||||
One of: `cubic`, `new_reno`, `bbr`
|
|
||||||
|
|
||||||
`cubic` is used by default.
|
|
||||||
|
|
||||||
#### udp_relay_mode
|
|
||||||
|
|
||||||
UDP packet relay mode
|
|
||||||
|
|
||||||
| Mode | Description |
|
|
||||||
|:-------|:-------------------------------------------------------------------------|
|
|
||||||
| native | native UDP characteristics |
|
|
||||||
| quic | lossless UDP relay using QUIC streams, additional overhead is introduced |
|
|
||||||
|
|
||||||
`native` is used by default.
|
|
||||||
|
|
||||||
#### network
|
|
||||||
|
|
||||||
Enabled network
|
|
||||||
|
|
||||||
One of `tcp` `udp`.
|
|
||||||
|
|
||||||
Both is enabled by default.
|
|
||||||
|
|
||||||
#### tls
|
|
||||||
|
|
||||||
==Required==
|
|
||||||
|
|
||||||
TLS configuration, see [TLS](/configuration/shared/tls/#outbound).
|
|
||||||
|
|
||||||
### Dial Fields
|
|
||||||
|
|
||||||
See [Dial Fields](/configuration/shared/dial) for details.
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
### 结构
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "tuic",
|
|
||||||
"tag": "tuic-out",
|
|
||||||
|
|
||||||
"server": "127.0.0.1",
|
|
||||||
"server_port": 1080,
|
|
||||||
"uuid": "2DD61D93-75D8-4DA4-AC0E-6AECE7EAC365",
|
|
||||||
"password": "hello",
|
|
||||||
"congestion_control": "cubic",
|
|
||||||
"udp_relay_mode": "native",
|
|
||||||
"zero_rtt_handshake": false,
|
|
||||||
"heartbeat": "10s",
|
|
||||||
"network": "tcp",
|
|
||||||
"tls": {},
|
|
||||||
|
|
||||||
... // 拨号字段
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! warning ""
|
|
||||||
|
|
||||||
默认安装不包含被 TUI 依赖的 QUIC,参阅 [安装](/zh/#_2)。
|
|
||||||
|
|
||||||
### 字段
|
|
||||||
|
|
||||||
#### server
|
|
||||||
|
|
||||||
==必填==
|
|
||||||
|
|
||||||
服务器地址。
|
|
||||||
|
|
||||||
#### server_port
|
|
||||||
|
|
||||||
==必填==
|
|
||||||
|
|
||||||
服务器端口。
|
|
||||||
|
|
||||||
#### uuid
|
|
||||||
|
|
||||||
==必填==
|
|
||||||
|
|
||||||
TUIC 用户 UUID
|
|
||||||
|
|
||||||
#### password
|
|
||||||
|
|
||||||
TUIC 用户密码
|
|
||||||
|
|
||||||
#### congestion_control
|
|
||||||
|
|
||||||
QUIC 流量控制算法
|
|
||||||
|
|
||||||
可选值: `cubic`, `new_reno`, `bbr`
|
|
||||||
|
|
||||||
默认使用 `cubic`。
|
|
||||||
|
|
||||||
#### udp_relay_mode
|
|
||||||
|
|
||||||
UDP 包中继模式
|
|
||||||
|
|
||||||
| 模式 | 描述 |
|
|
||||||
|--------|------------------------------|
|
|
||||||
| native | 原生 UDP |
|
|
||||||
| quic | 使用 QUIC 流的无损 UDP 中继,引入了额外的开销 |
|
|
||||||
|
|
||||||
|
|
||||||
#### zero_rtt_handshake
|
|
||||||
|
|
||||||
在客户端启用 0-RTT QUIC 连接握手
|
|
||||||
这对性能影响不大,因为协议是完全复用的
|
|
||||||
|
|
||||||
!!! warning ""
|
|
||||||
强烈建议禁用此功能,因为它容易受到重放攻击。
|
|
||||||
请参阅 [Attack of the clones](https://blog.cloudflare.com/even-faster-connection-establishment-with-quic-0-rtt-resumption/#attack-of-the-clones)
|
|
||||||
|
|
||||||
#### heartbeat
|
|
||||||
|
|
||||||
发送心跳包以保持连接存活的时间间隔
|
|
||||||
|
|
||||||
#### network
|
|
||||||
|
|
||||||
启用的网络协议。
|
|
||||||
|
|
||||||
`tcp` 或 `udp`。
|
|
||||||
|
|
||||||
默认所有。
|
|
||||||
|
|
||||||
#### tls
|
|
||||||
|
|
||||||
==必填==
|
|
||||||
|
|
||||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
|
|
||||||
|
|
||||||
### 拨号字段
|
|
||||||
|
|
||||||
参阅 [拨号字段](/zh/configuration/shared/dial/)。
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
"reuse_addr": false,
|
"reuse_addr": false,
|
||||||
"connect_timeout": "5s",
|
"connect_timeout": "5s",
|
||||||
"tcp_fast_open": false,
|
"tcp_fast_open": false,
|
||||||
"tcp_multi_path": false,
|
|
||||||
"udp_fragment": false,
|
"udp_fragment": false,
|
||||||
"domain_strategy": "prefer_ipv6",
|
"domain_strategy": "prefer_ipv6",
|
||||||
"fallback_delay": "300ms"
|
"fallback_delay": "300ms"
|
||||||
@@ -19,9 +18,9 @@
|
|||||||
|
|
||||||
### Fields
|
### Fields
|
||||||
|
|
||||||
| Field | Available Context |
|
| Field | Available Context |
|
||||||
|------------------------------------------------------------------------------------------------------------------------------------------|-------------------|
|
|----------------------------------------------------------------------------------------------------------------------|-------------------|
|
||||||
| `bind_interface` /`*bind_address` /`routing_mark` /`reuse_addr` / `tcp_fast_open` / `tcp_multi_path` / `udp_fragment` /`connect_timeout` | `detour` not set |
|
| `bind_interface` /`*bind_address` /`routing_mark` /`reuse_addr` / `tcp_fast_open`/ `udp_fragment` /`connect_timeout` | `detour` not set |
|
||||||
|
|
||||||
#### detour
|
#### detour
|
||||||
|
|
||||||
@@ -55,14 +54,6 @@ Reuse listener address.
|
|||||||
|
|
||||||
Enable TCP Fast Open.
|
Enable TCP Fast Open.
|
||||||
|
|
||||||
#### tcp_multi_path
|
|
||||||
|
|
||||||
!!! warning ""
|
|
||||||
|
|
||||||
Go 1.21 required.
|
|
||||||
|
|
||||||
Enable TCP Multi Path.
|
|
||||||
|
|
||||||
#### udp_fragment
|
#### udp_fragment
|
||||||
|
|
||||||
Enable UDP fragmentation.
|
Enable UDP fragmentation.
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
"reuse_addr": false,
|
"reuse_addr": false,
|
||||||
"connect_timeout": "5s",
|
"connect_timeout": "5s",
|
||||||
"tcp_fast_open": false,
|
"tcp_fast_open": false,
|
||||||
"tcp_multi_path": false,
|
|
||||||
"udp_fragment": false,
|
"udp_fragment": false,
|
||||||
"domain_strategy": "prefer_ipv6",
|
"domain_strategy": "prefer_ipv6",
|
||||||
"fallback_delay": "300ms"
|
"fallback_delay": "300ms"
|
||||||
@@ -19,9 +18,9 @@
|
|||||||
|
|
||||||
### 字段
|
### 字段
|
||||||
|
|
||||||
| 字段 | 可用上下文 |
|
| 字段 | 可用上下文 |
|
||||||
|------------------------------------------------------------------------------------------------------------------------------------------|--------------|
|
|----------------------------------------------------------------------------------------------------------------------|--------------|
|
||||||
| `bind_interface` /`*bind_address` /`routing_mark` /`reuse_addr` / `tcp_fast_open` / `tcp_mutli_path` / `udp_fragment` /`connect_timeout` | `detour` 未设置 |
|
| `bind_interface` /`*bind_address` /`routing_mark` /`reuse_addr` / `tcp_fast_open`/ `udp_fragment` /`connect_timeout` | `detour` 未设置 |
|
||||||
|
|
||||||
|
|
||||||
#### detour
|
#### detour
|
||||||
@@ -58,14 +57,6 @@
|
|||||||
|
|
||||||
启用 TCP Fast Open。
|
启用 TCP Fast Open。
|
||||||
|
|
||||||
#### tcp_multi_path
|
|
||||||
|
|
||||||
!!! warning ""
|
|
||||||
|
|
||||||
需要 Go 1.21。
|
|
||||||
|
|
||||||
启用 TCP Multi Path。
|
|
||||||
|
|
||||||
#### udp_fragment
|
#### udp_fragment
|
||||||
|
|
||||||
启用 UDP 分段。
|
启用 UDP 分段。
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
"listen": "::",
|
"listen": "::",
|
||||||
"listen_port": 5353,
|
"listen_port": 5353,
|
||||||
"tcp_fast_open": false,
|
"tcp_fast_open": false,
|
||||||
"tcp_multi_path": false,
|
|
||||||
"udp_fragment": false,
|
"udp_fragment": false,
|
||||||
"sniff": false,
|
"sniff": false,
|
||||||
"sniff_override_destination": false,
|
"sniff_override_destination": false,
|
||||||
@@ -25,7 +24,6 @@
|
|||||||
| `listen` | Needs to listen on TCP or UDP. |
|
| `listen` | Needs to listen on TCP or UDP. |
|
||||||
| `listen_port` | Needs to listen on TCP or UDP. |
|
| `listen_port` | Needs to listen on TCP or UDP. |
|
||||||
| `tcp_fast_open` | Needs to listen on TCP. |
|
| `tcp_fast_open` | Needs to listen on TCP. |
|
||||||
| `tcp_multi_path` | Needs to listen on TCP. |
|
|
||||||
| `udp_timeout` | Needs to assemble UDP connections, currently Tun and Shadowsocks. |
|
| `udp_timeout` | Needs to assemble UDP connections, currently Tun and Shadowsocks. |
|
||||||
| `proxy_protocol` | Needs to listen on TCP. |
|
| `proxy_protocol` | Needs to listen on TCP. |
|
||||||
| `proxy_protocol_accept_no_header` | When `proxy_protocol` enabled |
|
| `proxy_protocol_accept_no_header` | When `proxy_protocol` enabled |
|
||||||
@@ -44,14 +42,6 @@ Listen port.
|
|||||||
|
|
||||||
Enable TCP Fast Open.
|
Enable TCP Fast Open.
|
||||||
|
|
||||||
#### tcp_multi_path
|
|
||||||
|
|
||||||
!!! warning ""
|
|
||||||
|
|
||||||
Go 1.21 required.
|
|
||||||
|
|
||||||
Enable TCP Multi Path.
|
|
||||||
|
|
||||||
#### udp_fragment
|
#### udp_fragment
|
||||||
|
|
||||||
Enable UDP fragmentation.
|
Enable UDP fragmentation.
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
"listen": "::",
|
"listen": "::",
|
||||||
"listen_port": 5353,
|
"listen_port": 5353,
|
||||||
"tcp_fast_open": false,
|
"tcp_fast_open": false,
|
||||||
"tcp_multi_path": false,
|
|
||||||
"udp_fragment": false,
|
"udp_fragment": false,
|
||||||
"sniff": false,
|
"sniff": false,
|
||||||
"sniff_override_destination": false,
|
"sniff_override_destination": false,
|
||||||
@@ -24,7 +23,6 @@
|
|||||||
| `listen` | 需要监听 TCP 或 UDP。 |
|
| `listen` | 需要监听 TCP 或 UDP。 |
|
||||||
| `listen_port` | 需要监听 TCP 或 UDP。 |
|
| `listen_port` | 需要监听 TCP 或 UDP。 |
|
||||||
| `tcp_fast_open` | 需要监听 TCP。 |
|
| `tcp_fast_open` | 需要监听 TCP。 |
|
||||||
| `tcp_multi_path` | 需要监听 TCP。 |
|
|
||||||
| `udp_timeout` | 需要组装 UDP 连接, 当前为 Tun 和 Shadowsocks。 |
|
| `udp_timeout` | 需要组装 UDP 连接, 当前为 Tun 和 Shadowsocks。 |
|
||||||
| `proxy_protocol` | 需要监听 TCP。 |
|
| `proxy_protocol` | 需要监听 TCP。 |
|
||||||
| `proxy_protocol_accept_no_header` | `proxy_protocol` 启用时 |
|
| `proxy_protocol_accept_no_header` | `proxy_protocol` 启用时 |
|
||||||
@@ -45,14 +43,6 @@
|
|||||||
|
|
||||||
启用 TCP Fast Open。
|
启用 TCP Fast Open。
|
||||||
|
|
||||||
#### tcp_multi_path
|
|
||||||
|
|
||||||
!!! warning ""
|
|
||||||
|
|
||||||
需要 Go 1.21。
|
|
||||||
|
|
||||||
启用 TCP Multi Path。
|
|
||||||
|
|
||||||
#### udp_fragment
|
#### udp_fragment
|
||||||
|
|
||||||
启用 UDP 分段。
|
启用 UDP 分段。
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ Multiplex protocol.
|
|||||||
| yamux | https://github.com/hashicorp/yamux |
|
| yamux | https://github.com/hashicorp/yamux |
|
||||||
| h2mux | https://golang.org/x/net/http2 |
|
| h2mux | https://golang.org/x/net/http2 |
|
||||||
|
|
||||||
h2mux is used by default.
|
SMux is used by default.
|
||||||
|
|
||||||
#### max_connections
|
#### max_connections
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
| yamux | https://github.com/hashicorp/yamux |
|
| yamux | https://github.com/hashicorp/yamux |
|
||||||
| h2mux | https://golang.org/x/net/http2 |
|
| h2mux | https://golang.org/x/net/http2 |
|
||||||
|
|
||||||
默认使用 h2mux。
|
默认使用 SMux。
|
||||||
|
|
||||||
#### max_connections
|
#### max_connections
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -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)
|
|
||||||
|
|||||||
@@ -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)
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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 被缓存的环境中丢失记录。
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ Experimental Android client for sing-box.
|
|||||||
#### Download
|
#### Download
|
||||||
|
|
||||||
* [AppCenter](https://install.appcenter.ms/users/nekohasekai/apps/sfa/distribution_groups/publictest)
|
* [AppCenter](https://install.appcenter.ms/users/nekohasekai/apps/sfa/distribution_groups/publictest)
|
||||||
* [Github Releases](https://SagerNet/sing-box/releases)
|
|
||||||
|
|
||||||
#### Note
|
#### Note
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
#### 下载
|
#### 下载
|
||||||
|
|
||||||
* [AppCenter](https://install.appcenter.ms/users/nekohasekai/apps/sfa/distribution_groups/publictest)
|
* [AppCenter](https://install.appcenter.ms/users/nekohasekai/apps/sfa/distribution_groups/publictest)
|
||||||
* [Github Releases](https://SagerNet/sing-box/releases)
|
|
||||||
|
|
||||||
#### 注意事项
|
#### 注意事项
|
||||||
|
|
||||||
|
|||||||
@@ -5,16 +5,15 @@ 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)`
|
* User Agent in remote profile request is `SFA/$version ($version_code; sing-box $sing_box_version)`
|
||||||
* Crash logs is located in `Settings` -> `View Service Log`
|
* Crash logs is located in `Settings` -> `View Service Log`
|
||||||
|
|
||||||
#### Privacy policy
|
#### Privacy policy
|
||||||
|
|||||||
@@ -5,12 +5,11 @@
|
|||||||
#### 要求
|
#### 要求
|
||||||
|
|
||||||
* 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)
|
|
||||||
|
|
||||||
#### 注意事项
|
#### 注意事项
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
|
||||||
@@ -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 不收集或共享个人数据。
|
|
||||||
* 软件生成的数据始终在您的设备上。
|
|
||||||
@@ -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.
|
|
||||||
@@ -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
|
|
||||||
```
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
# Android
|
|
||||||
|
|
||||||
## Termux
|
|
||||||
|
|
||||||
```shell
|
|
||||||
pkg add sing-box
|
|
||||||
```
|
|
||||||
@@ -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
|
|
||||||
```
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
# Windows
|
|
||||||
|
|
||||||
## Chocolatey
|
|
||||||
|
|
||||||
```shell
|
|
||||||
choco install sing-box
|
|
||||||
```
|
|
||||||
|
|
||||||
## winget
|
|
||||||
|
|
||||||
```shell
|
|
||||||
winget install sing-box
|
|
||||||
```
|
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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,16 +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
|
|
||||||
saveMetadataTimer *time.Timer
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
@@ -40,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
|
||||||
}
|
}
|
||||||
@@ -121,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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,27 +3,20 @@ package cachefile
|
|||||||
import (
|
import (
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
|
||||||
|
|
||||||
"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
|
||||||
@@ -32,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 {
|
||||||
@@ -58,69 +47,17 @@ func (c *CacheFile) FakeIPSaveMetadata(metadata *adapter.FakeIPMetadata) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CacheFile) FakeIPSaveMetadataAsync(metadata *adapter.FakeIPMetadata) {
|
|
||||||
if timer := c.saveMetadataTimer; timer != nil {
|
|
||||||
timer.Stop()
|
|
||||||
}
|
|
||||||
c.saveMetadataTimer = time.AfterFunc(10*time.Second, func() {
|
|
||||||
_ = c.FakeIPSaveMetadata(metadata)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *CacheFile) FakeIPStore(address netip.Addr, domain string) error {
|
func (c *CacheFile) FakeIPStore(address netip.Addr, domain string) error {
|
||||||
return c.DB.Batch(func(tx *bbolt.Tx) error {
|
return c.DB.Batch(func(tx *bbolt.Tx) error {
|
||||||
bucket, err := tx.CreateBucketIfNotExists(bucketFakeIP)
|
bucket, err := tx.CreateBucketIfNotExists(bucketFakeIP)
|
||||||
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)
|
||||||
@@ -133,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)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,10 +63,38 @@ func proxyInfo(server *Server, detour adapter.Outbound) *badjson.JSONObject {
|
|||||||
var info badjson.JSONObject
|
var info badjson.JSONObject
|
||||||
var clashType string
|
var clashType string
|
||||||
switch detour.Type() {
|
switch detour.Type() {
|
||||||
|
case C.TypeDirect:
|
||||||
|
clashType = "Direct"
|
||||||
case C.TypeBlock:
|
case C.TypeBlock:
|
||||||
clashType = "Reject"
|
clashType = "Reject"
|
||||||
|
case C.TypeSocks:
|
||||||
|
clashType = "Socks"
|
||||||
|
case C.TypeHTTP:
|
||||||
|
clashType = "HTTP"
|
||||||
|
case C.TypeShadowsocks:
|
||||||
|
clashType = "Shadowsocks"
|
||||||
|
case C.TypeVMess:
|
||||||
|
clashType = "VMess"
|
||||||
|
case C.TypeTrojan:
|
||||||
|
clashType = "Trojan"
|
||||||
|
case C.TypeHysteria:
|
||||||
|
clashType = "Hysteria"
|
||||||
|
case C.TypeWireGuard:
|
||||||
|
clashType = "WireGuard"
|
||||||
|
case C.TypeShadowsocksR:
|
||||||
|
clashType = "ShadowsocksR"
|
||||||
|
case C.TypeVLESS:
|
||||||
|
clashType = "VLESS"
|
||||||
|
case C.TypeTor:
|
||||||
|
clashType = "Tor"
|
||||||
|
case C.TypeSSH:
|
||||||
|
clashType = "SSH"
|
||||||
|
case C.TypeSelector:
|
||||||
|
clashType = "Selector"
|
||||||
|
case C.TypeURLTest:
|
||||||
|
clashType = "URLTest"
|
||||||
default:
|
default:
|
||||||
clashType = C.ProxyDisplayName(detour.Type())
|
clashType = "Direct"
|
||||||
}
|
}
|
||||||
info.Put("type", clashType)
|
info.Put("type", clashType)
|
||||||
info.Put("name", detour.Tag())
|
info.Put("name", detour.Tag())
|
||||||
@@ -106,7 +134,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
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +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/sing/service/filemanager"
|
||||||
"github.com/sagernet/websocket"
|
"github.com/sagernet/websocket"
|
||||||
|
|
||||||
@@ -49,10 +48,8 @@ type Server struct {
|
|||||||
storeSelected bool
|
storeSelected bool
|
||||||
storeFakeIP bool
|
storeFakeIP bool
|
||||||
cacheFilePath string
|
cacheFilePath string
|
||||||
cacheID string
|
|
||||||
cacheFile adapter.ClashCacheFile
|
cacheFile adapter.ClashCacheFile
|
||||||
|
|
||||||
externalController bool
|
|
||||||
externalUI string
|
externalUI string
|
||||||
externalUIDownloadURL string
|
externalUIDownloadURL string
|
||||||
externalUIDownloadDetour string
|
externalUIDownloadDetour string
|
||||||
@@ -70,17 +67,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,
|
||||||
externalController: options.ExternalController != "",
|
|
||||||
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"
|
||||||
}
|
}
|
||||||
@@ -95,7 +88,6 @@ func NewServer(ctx context.Context, router adapter.Router, logFactory log.Observ
|
|||||||
cachePath = filemanager.BasePath(ctx, cachePath)
|
cachePath = filemanager.BasePath(ctx, 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{"*"},
|
||||||
@@ -113,7 +105,7 @@ 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())
|
||||||
@@ -138,7 +130,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")
|
||||||
}
|
}
|
||||||
@@ -148,20 +140,18 @@ func (s *Server) PreStart() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) Start() error {
|
func (s *Server) Start() error {
|
||||||
if s.externalController {
|
s.checkAndDownloadExternalUI()
|
||||||
s.checkAndDownloadExternalUI()
|
listener, err := net.Listen("tcp", s.httpServer.Addr)
|
||||||
listener, err := net.Listen("tcp", s.httpServer.Addr)
|
if err != nil {
|
||||||
if err != nil {
|
return E.Cause(err, "external controller listen error")
|
||||||
return E.Cause(err, "external controller listen error")
|
|
||||||
}
|
|
||||||
s.logger.Info("restful api listening at ", listener.Addr())
|
|
||||||
go func() {
|
|
||||||
err = s.httpServer.Serve(listener)
|
|
||||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
|
||||||
s.logger.Error("external controller serve error: ", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
|
s.logger.Info("restful api listening at ", listener.Addr())
|
||||||
|
go func() {
|
||||||
|
err = s.httpServer.Serve(listener)
|
||||||
|
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
|
s.logger.Error("external controller serve error: ", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,10 +183,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
|
||||||
|
|||||||
@@ -55,14 +55,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 {
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ 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/v5"
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -3,9 +3,7 @@ package libbox
|
|||||||
const (
|
const (
|
||||||
CommandLog int32 = iota
|
CommandLog int32 = iota
|
||||||
CommandStatus
|
CommandStatus
|
||||||
|
CommandServiceStop
|
||||||
CommandServiceReload
|
CommandServiceReload
|
||||||
CommandCloseConnections
|
CommandCloseConnections
|
||||||
CommandGroup
|
|
||||||
CommandSelectOutbound
|
|
||||||
CommandURLTest
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,9 +10,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type CommandClient struct {
|
type CommandClient struct {
|
||||||
handler CommandClientHandler
|
sharedDirectory string
|
||||||
conn net.Conn
|
handler CommandClientHandler
|
||||||
options CommandClientOptions
|
conn net.Conn
|
||||||
|
options CommandClientOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
type CommandClientOptions struct {
|
type CommandClientOptions struct {
|
||||||
@@ -25,34 +26,26 @@ type CommandClientHandler interface {
|
|||||||
Disconnected(message string)
|
Disconnected(message string)
|
||||||
WriteLog(message string)
|
WriteLog(message string)
|
||||||
WriteStatus(message *StatusMessage)
|
WriteStatus(message *StatusMessage)
|
||||||
WriteGroups(message OutboundGroupIterator)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewStandaloneCommandClient() *CommandClient {
|
|
||||||
return new(CommandClient)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCommandClient(sharedDirectory string, handler CommandClientHandler, options *CommandClientOptions) *CommandClient {
|
func NewCommandClient(sharedDirectory string, handler CommandClientHandler, options *CommandClientOptions) *CommandClient {
|
||||||
return &CommandClient{
|
return &CommandClient{
|
||||||
handler: handler,
|
sharedDirectory: sharedDirectory,
|
||||||
options: common.PtrValueOrDefault(options),
|
handler: handler,
|
||||||
|
options: common.PtrValueOrDefault(options),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CommandClient) directConnect() (net.Conn, error) {
|
func clientConnect(sharedDirectory string) (net.Conn, error) {
|
||||||
if !sTVOS {
|
return net.DialUnix("unix", nil, &net.UnixAddr{
|
||||||
return net.DialUnix("unix", nil, &net.UnixAddr{
|
Name: filepath.Join(sharedDirectory, "command.sock"),
|
||||||
Name: filepath.Join(sBasePath, "command.sock"),
|
Net: "unix",
|
||||||
Net: "unix",
|
})
|
||||||
})
|
|
||||||
} else {
|
|
||||||
return net.Dial("tcp", "127.0.0.1:8964")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CommandClient) Connect() error {
|
func (c *CommandClient) Connect() error {
|
||||||
common.Close(c.conn)
|
common.Close(c.conn)
|
||||||
conn, err := c.directConnect()
|
conn, err := clientConnect(c.sharedDirectory)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -72,13 +65,6 @@ func (c *CommandClient) Connect() error {
|
|||||||
}
|
}
|
||||||
c.handler.Connected()
|
c.handler.Connected()
|
||||||
go c.handleStatusConn(conn)
|
go c.handleStatusConn(conn)
|
||||||
case CommandGroup:
|
|
||||||
err = binary.Write(conn, binary.BigEndian, c.options.StatusInterval)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "write interval")
|
|
||||||
}
|
|
||||||
c.handler.Connected()
|
|
||||||
go c.handleGroupConn(conn)
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import (
|
|||||||
"github.com/sagernet/sing-box/common/dialer/conntrack"
|
"github.com/sagernet/sing-box/common/dialer/conntrack"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *CommandClient) CloseConnections() error {
|
func ClientCloseConnections(sharedDirectory string) error {
|
||||||
conn, err := c.directConnect()
|
conn, err := clientConnect(sharedDirectory)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,234 +0,0 @@
|
|||||||
package libbox
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/binary"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
"github.com/sagernet/sing-box/common/urltest"
|
|
||||||
"github.com/sagernet/sing-box/outbound"
|
|
||||||
"github.com/sagernet/sing/common/rw"
|
|
||||||
"github.com/sagernet/sing/service"
|
|
||||||
)
|
|
||||||
|
|
||||||
type OutboundGroup struct {
|
|
||||||
Tag string
|
|
||||||
Type string
|
|
||||||
Selectable bool
|
|
||||||
Selected string
|
|
||||||
items []*OutboundGroupItem
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *OutboundGroup) GetItems() OutboundGroupItemIterator {
|
|
||||||
return newIterator(g.items)
|
|
||||||
}
|
|
||||||
|
|
||||||
type OutboundGroupIterator interface {
|
|
||||||
Next() *OutboundGroup
|
|
||||||
HasNext() bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type OutboundGroupItem struct {
|
|
||||||
Tag string
|
|
||||||
Type string
|
|
||||||
URLTestTime int64
|
|
||||||
URLTestDelay int32
|
|
||||||
}
|
|
||||||
|
|
||||||
type OutboundGroupItemIterator interface {
|
|
||||||
Next() *OutboundGroupItem
|
|
||||||
HasNext() bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *CommandClient) handleGroupConn(conn net.Conn) {
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
for {
|
|
||||||
groups, err := readGroups(conn)
|
|
||||||
if err != nil {
|
|
||||||
c.handler.Disconnected(err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.handler.WriteGroups(groups)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *CommandServer) handleGroupConn(conn net.Conn) error {
|
|
||||||
defer conn.Close()
|
|
||||||
ctx := connKeepAlive(conn)
|
|
||||||
for {
|
|
||||||
service := s.service
|
|
||||||
if service != nil {
|
|
||||||
err := writeGroups(conn, service)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
err := binary.Write(conn, binary.BigEndian, uint16(0))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return ctx.Err()
|
|
||||||
case <-time.After(2 * time.Second):
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return ctx.Err()
|
|
||||||
case <-s.urlTestUpdate:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func readGroups(reader io.Reader) (OutboundGroupIterator, error) {
|
|
||||||
var groupLength uint16
|
|
||||||
err := binary.Read(reader, binary.BigEndian, &groupLength)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
groups := make([]*OutboundGroup, 0, groupLength)
|
|
||||||
for i := 0; i < int(groupLength); i++ {
|
|
||||||
var group OutboundGroup
|
|
||||||
group.Tag, err = rw.ReadVString(reader)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
group.Type, err = rw.ReadVString(reader)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = binary.Read(reader, binary.BigEndian, &group.Selectable)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
group.Selected, err = rw.ReadVString(reader)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var itemLength uint16
|
|
||||||
err = binary.Read(reader, binary.BigEndian, &itemLength)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
group.items = make([]*OutboundGroupItem, itemLength)
|
|
||||||
for j := 0; j < int(itemLength); j++ {
|
|
||||||
var item OutboundGroupItem
|
|
||||||
item.Tag, err = rw.ReadVString(reader)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
item.Type, err = rw.ReadVString(reader)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = binary.Read(reader, binary.BigEndian, &item.URLTestTime)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = binary.Read(reader, binary.BigEndian, &item.URLTestDelay)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
group.items[j] = &item
|
|
||||||
}
|
|
||||||
groups = append(groups, &group)
|
|
||||||
}
|
|
||||||
return newIterator(groups), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeGroups(writer io.Writer, boxService *BoxService) error {
|
|
||||||
historyStorage := service.PtrFromContext[urltest.HistoryStorage](boxService.ctx)
|
|
||||||
|
|
||||||
outbounds := boxService.instance.Router().Outbounds()
|
|
||||||
var iGroups []adapter.OutboundGroup
|
|
||||||
for _, it := range outbounds {
|
|
||||||
if group, isGroup := it.(adapter.OutboundGroup); isGroup {
|
|
||||||
iGroups = append(iGroups, group)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var groups []OutboundGroup
|
|
||||||
for _, iGroup := range iGroups {
|
|
||||||
var group OutboundGroup
|
|
||||||
group.Tag = iGroup.Tag()
|
|
||||||
group.Type = iGroup.Type()
|
|
||||||
_, group.Selectable = iGroup.(*outbound.Selector)
|
|
||||||
group.Selected = iGroup.Now()
|
|
||||||
|
|
||||||
for _, itemTag := range iGroup.All() {
|
|
||||||
itemOutbound, isLoaded := boxService.instance.Router().Outbound(itemTag)
|
|
||||||
if !isLoaded {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var item OutboundGroupItem
|
|
||||||
item.Tag = itemTag
|
|
||||||
item.Type = itemOutbound.Type()
|
|
||||||
if history := historyStorage.LoadURLTestHistory(adapter.OutboundTag(itemOutbound)); history != nil {
|
|
||||||
item.URLTestTime = history.Time.Unix()
|
|
||||||
item.URLTestDelay = int32(history.Delay)
|
|
||||||
}
|
|
||||||
group.items = append(group.items, &item)
|
|
||||||
}
|
|
||||||
groups = append(groups, group)
|
|
||||||
}
|
|
||||||
|
|
||||||
err := binary.Write(writer, binary.BigEndian, uint16(len(groups)))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, group := range groups {
|
|
||||||
err = rw.WriteVString(writer, group.Tag)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = rw.WriteVString(writer, group.Type)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = binary.Write(writer, binary.BigEndian, group.Selectable)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = rw.WriteVString(writer, group.Selected)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = binary.Write(writer, binary.BigEndian, uint16(len(group.items)))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, item := range group.items {
|
|
||||||
err = rw.WriteVString(writer, item.Tag)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = rw.WriteVString(writer, item.Type)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = binary.Write(writer, binary.BigEndian, item.URLTestTime)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = binary.Write(writer, binary.BigEndian, item.URLTestDelay)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -11,7 +11,7 @@ func (s *CommandServer) WriteMessage(message string) {
|
|||||||
s.subscriber.Emit(message)
|
s.subscriber.Emit(message)
|
||||||
s.access.Lock()
|
s.access.Lock()
|
||||||
s.savedLines.PushBack(message)
|
s.savedLines.PushBack(message)
|
||||||
if s.savedLines.Len() > s.maxLines {
|
if s.savedLines.Len() > 100 {
|
||||||
s.savedLines.Remove(s.savedLines.Front())
|
s.savedLines.Remove(s.savedLines.Front())
|
||||||
}
|
}
|
||||||
s.access.Unlock()
|
s.access.Unlock()
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import (
|
|||||||
"github.com/sagernet/sing/common/rw"
|
"github.com/sagernet/sing/common/rw"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *CommandClient) ServiceReload() error {
|
func ClientServiceReload(sharedDirectory string) error {
|
||||||
conn, err := c.directConnect()
|
conn, err := clientConnect(sharedDirectory)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
package libbox
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/binary"
|
|
||||||
"net"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/outbound"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
"github.com/sagernet/sing/common/rw"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (c *CommandClient) SelectOutbound(groupTag string, outboundTag string) error {
|
|
||||||
conn, err := c.directConnect()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
err = binary.Write(conn, binary.BigEndian, uint8(CommandSelectOutbound))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = rw.WriteVString(conn, groupTag)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = rw.WriteVString(conn, outboundTag)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return readError(conn)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *CommandServer) handleSelectOutbound(conn net.Conn) error {
|
|
||||||
defer conn.Close()
|
|
||||||
groupTag, err := rw.ReadVString(conn)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
outboundTag, err := rw.ReadVString(conn)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
service := s.service
|
|
||||||
if service == nil {
|
|
||||||
return writeError(conn, E.New("service not ready"))
|
|
||||||
}
|
|
||||||
outboundGroup, isLoaded := service.instance.Router().Outbound(groupTag)
|
|
||||||
if !isLoaded {
|
|
||||||
return writeError(conn, E.New("selector not found: ", groupTag))
|
|
||||||
}
|
|
||||||
selector, isSelector := outboundGroup.(*outbound.Selector)
|
|
||||||
if !isSelector {
|
|
||||||
return writeError(conn, E.New("outbound is not a selector: ", groupTag))
|
|
||||||
}
|
|
||||||
if !selector.SelectOutbound(outboundTag) {
|
|
||||||
return writeError(conn, E.New("outbound not found in selector: ", outboundTag))
|
|
||||||
}
|
|
||||||
return writeError(conn, nil)
|
|
||||||
}
|
|
||||||
@@ -7,101 +7,49 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/urltest"
|
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
"github.com/sagernet/sing/common/debug"
|
"github.com/sagernet/sing/common/debug"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
"github.com/sagernet/sing/common/observable"
|
"github.com/sagernet/sing/common/observable"
|
||||||
"github.com/sagernet/sing/common/x/list"
|
"github.com/sagernet/sing/common/x/list"
|
||||||
"github.com/sagernet/sing/service"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type CommandServer struct {
|
type CommandServer struct {
|
||||||
|
sockPath string
|
||||||
listener net.Listener
|
listener net.Listener
|
||||||
handler CommandServerHandler
|
handler CommandServerHandler
|
||||||
|
|
||||||
access sync.Mutex
|
access sync.Mutex
|
||||||
savedLines *list.List[string]
|
savedLines *list.List[string]
|
||||||
maxLines int
|
|
||||||
subscriber *observable.Subscriber[string]
|
subscriber *observable.Subscriber[string]
|
||||||
observer *observable.Observer[string]
|
observer *observable.Observer[string]
|
||||||
service *BoxService
|
|
||||||
|
|
||||||
urlTestListener *list.Element[func()]
|
|
||||||
urlTestUpdate chan struct{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type CommandServerHandler interface {
|
type CommandServerHandler interface {
|
||||||
|
ServiceStop() error
|
||||||
ServiceReload() error
|
ServiceReload() error
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCommandServer(handler CommandServerHandler, maxLines int32) *CommandServer {
|
func NewCommandServer(sharedDirectory string, handler CommandServerHandler) *CommandServer {
|
||||||
server := &CommandServer{
|
server := &CommandServer{
|
||||||
handler: handler,
|
sockPath: filepath.Join(sharedDirectory, "command.sock"),
|
||||||
savedLines: new(list.List[string]),
|
handler: handler,
|
||||||
maxLines: int(maxLines),
|
savedLines: new(list.List[string]),
|
||||||
subscriber: observable.NewSubscriber[string](128),
|
subscriber: observable.NewSubscriber[string](128),
|
||||||
urlTestUpdate: make(chan struct{}, 1),
|
|
||||||
}
|
}
|
||||||
server.observer = observable.NewObserver[string](server.subscriber, 64)
|
server.observer = observable.NewObserver[string](server.subscriber, 64)
|
||||||
return server
|
return server
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *CommandServer) SetService(newService *BoxService) {
|
|
||||||
if s.service != nil && s.listener != nil {
|
|
||||||
service.PtrFromContext[urltest.HistoryStorage](s.service.ctx).RemoveListener(s.urlTestListener)
|
|
||||||
s.urlTestListener = nil
|
|
||||||
}
|
|
||||||
s.service = newService
|
|
||||||
if newService != nil {
|
|
||||||
s.urlTestListener = service.PtrFromContext[urltest.HistoryStorage](newService.ctx).AddListener(s.notifyURLTestUpdate)
|
|
||||||
}
|
|
||||||
s.notifyURLTestUpdate()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *CommandServer) notifyURLTestUpdate() {
|
|
||||||
select {
|
|
||||||
case s.urlTestUpdate <- struct{}{}:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *CommandServer) Start() error {
|
func (s *CommandServer) Start() error {
|
||||||
if !sTVOS {
|
os.Remove(s.sockPath)
|
||||||
return s.listenUNIX()
|
|
||||||
} else {
|
|
||||||
return s.listenTCP()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *CommandServer) listenUNIX() error {
|
|
||||||
sockPath := filepath.Join(sBasePath, "command.sock")
|
|
||||||
os.Remove(sockPath)
|
|
||||||
listener, err := net.ListenUnix("unix", &net.UnixAddr{
|
listener, err := net.ListenUnix("unix", &net.UnixAddr{
|
||||||
Name: sockPath,
|
Name: s.sockPath,
|
||||||
Net: "unix",
|
Net: "unix",
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, "listen ", sockPath)
|
return err
|
||||||
}
|
|
||||||
if sUserID > 0 {
|
|
||||||
err = os.Chown(sockPath, sUserID, sGroupID)
|
|
||||||
if err != nil {
|
|
||||||
listener.Close()
|
|
||||||
os.Remove(sockPath)
|
|
||||||
return E.Cause(err, "chown")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s.listener = listener
|
|
||||||
go s.loopConnection(listener)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *CommandServer) listenTCP() error {
|
|
||||||
listener, err := net.Listen("tcp", "127.0.0.1:8964")
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "listen")
|
|
||||||
}
|
}
|
||||||
s.listener = listener
|
s.listener = listener
|
||||||
go s.loopConnection(listener)
|
go s.loopConnection(listener)
|
||||||
@@ -144,16 +92,12 @@ func (s *CommandServer) handleConnection(conn net.Conn) error {
|
|||||||
return s.handleLogConn(conn)
|
return s.handleLogConn(conn)
|
||||||
case CommandStatus:
|
case CommandStatus:
|
||||||
return s.handleStatusConn(conn)
|
return s.handleStatusConn(conn)
|
||||||
|
case CommandServiceStop:
|
||||||
|
return s.handleServiceStop(conn)
|
||||||
case CommandServiceReload:
|
case CommandServiceReload:
|
||||||
return s.handleServiceReload(conn)
|
return s.handleServiceReload(conn)
|
||||||
case CommandCloseConnections:
|
case CommandCloseConnections:
|
||||||
return s.handleCloseConnections(conn)
|
return s.handleCloseConnections(conn)
|
||||||
case CommandGroup:
|
|
||||||
return s.handleGroupConn(conn)
|
|
||||||
case CommandSelectOutbound:
|
|
||||||
return s.handleSelectOutbound(conn)
|
|
||||||
case CommandURLTest:
|
|
||||||
return s.handleURLTest(conn)
|
|
||||||
default:
|
default:
|
||||||
return E.New("unknown command: ", command)
|
return E.New("unknown command: ", command)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
package libbox
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/binary"
|
|
||||||
"io"
|
|
||||||
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
"github.com/sagernet/sing/common/rw"
|
|
||||||
)
|
|
||||||
|
|
||||||
func readError(reader io.Reader) error {
|
|
||||||
var hasError bool
|
|
||||||
err := binary.Read(reader, binary.BigEndian, &hasError)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if hasError {
|
|
||||||
errorMessage, err := rw.ReadVString(reader)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return E.New(errorMessage)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeError(writer io.Writer, wErr error) error {
|
|
||||||
err := binary.Write(writer, binary.BigEndian, wErr != nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if wErr != nil {
|
|
||||||
err = rw.WriteVString(writer, wErr.Error())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -7,40 +7,22 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/dialer/conntrack"
|
"github.com/sagernet/sing-box/common/dialer/conntrack"
|
||||||
"github.com/sagernet/sing-box/experimental/clashapi"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
)
|
)
|
||||||
|
|
||||||
type StatusMessage struct {
|
type StatusMessage struct {
|
||||||
Memory int64
|
Memory int64
|
||||||
Goroutines int32
|
Goroutines int32
|
||||||
ConnectionsIn int32
|
Connections int32
|
||||||
ConnectionsOut int32
|
|
||||||
TrafficAvailable bool
|
|
||||||
Uplink int64
|
|
||||||
Downlink int64
|
|
||||||
UplinkTotal int64
|
|
||||||
DownlinkTotal int64
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *CommandServer) readStatus() StatusMessage {
|
func readStatus() StatusMessage {
|
||||||
var memStats runtime.MemStats
|
var memStats runtime.MemStats
|
||||||
runtime.ReadMemStats(&memStats)
|
runtime.ReadMemStats(&memStats)
|
||||||
var message StatusMessage
|
var message StatusMessage
|
||||||
message.Memory = int64(memStats.StackInuse + memStats.HeapInuse + memStats.HeapIdle - memStats.HeapReleased)
|
message.Memory = int64(memStats.StackInuse + memStats.HeapInuse + memStats.HeapIdle - memStats.HeapReleased)
|
||||||
message.Goroutines = int32(runtime.NumGoroutine())
|
message.Goroutines = int32(runtime.NumGoroutine())
|
||||||
message.ConnectionsOut = int32(conntrack.Count())
|
message.Connections = int32(conntrack.Count())
|
||||||
|
|
||||||
if s.service != nil {
|
|
||||||
if clashServer := s.service.instance.Router().ClashServer(); clashServer != nil {
|
|
||||||
message.TrafficAvailable = true
|
|
||||||
trafficManager := clashServer.(*clashapi.Server).TrafficManager()
|
|
||||||
message.Uplink, message.Downlink = trafficManager.Now()
|
|
||||||
message.UplinkTotal, message.DownlinkTotal = trafficManager.Total()
|
|
||||||
message.ConnectionsIn = int32(trafficManager.Connections())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return message
|
return message
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,7 +36,7 @@ func (s *CommandServer) handleStatusConn(conn net.Conn) error {
|
|||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
ctx := connKeepAlive(conn)
|
ctx := connKeepAlive(conn)
|
||||||
for {
|
for {
|
||||||
err = binary.Write(conn, binary.BigEndian, s.readStatus())
|
err = binary.Write(conn, binary.BigEndian, readStatus())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
48
experimental/libbox/command_stop.go
Normal file
48
experimental/libbox/command_stop.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package libbox
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"net"
|
||||||
|
"runtime/debug"
|
||||||
|
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/rw"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ClientServiceStop(sharedDirectory string) error {
|
||||||
|
conn, err := clientConnect(sharedDirectory)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
err = binary.Write(conn, binary.BigEndian, uint8(CommandServiceStop))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var hasError bool
|
||||||
|
err = binary.Read(conn, binary.BigEndian, &hasError)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if hasError {
|
||||||
|
errorMessage, err := rw.ReadVString(conn)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return E.New(errorMessage)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CommandServer) handleServiceStop(conn net.Conn) error {
|
||||||
|
rErr := s.handler.ServiceStop()
|
||||||
|
err := binary.Write(conn, binary.BigEndian, rErr != nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if rErr != nil {
|
||||||
|
return rw.WriteVString(conn, rErr.Error())
|
||||||
|
}
|
||||||
|
debug.FreeOSMemory()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user