Compare commits
119 Commits
v1.2.7
...
v1.4.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d85fa16ce | ||
|
|
3442c7a692 | ||
|
|
c485c11a0c | ||
|
|
3e43a676a6 | ||
|
|
5acc40d72f | ||
|
|
2d0aa96cbe | ||
|
|
a7e16ee502 | ||
|
|
56974ff465 | ||
|
|
6de6804212 | ||
|
|
3e6d34adea | ||
|
|
6ec8ff79b5 | ||
|
|
2675aff98a | ||
|
|
09ffa2c66e | ||
|
|
9fba4f02b6 | ||
|
|
59987747e5 | ||
|
|
c40140bbae | ||
|
|
2123b216c0 | ||
|
|
1983f54907 | ||
|
|
8d629ef323 | ||
|
|
f57bee2f4b | ||
|
|
679739683e | ||
|
|
4fcce1f073 | ||
|
|
ff14220e08 | ||
|
|
a7b7a5c3c5 | ||
|
|
b054441f34 | ||
|
|
1e31d26e03 | ||
|
|
ffe515d0e0 | ||
|
|
aad021f521 | ||
|
|
4a986459ee | ||
|
|
9532d0cba4 | ||
|
|
cadc34f3ad | ||
|
|
db23a48b36 | ||
|
|
407cf68e59 | ||
|
|
e0058ca9c5 | ||
|
|
8140af01aa | ||
|
|
98bf696d01 | ||
|
|
e075bb5c8d | ||
|
|
c6baabedef | ||
|
|
6e6998dab7 | ||
|
|
1a29c23263 | ||
|
|
0f87396ab6 | ||
|
|
ffde948860 | ||
|
|
69b5dbdcc3 | ||
|
|
1121517755 | ||
|
|
6879def619 | ||
|
|
5c0f6d0a6f | ||
|
|
d74abbd20e | ||
|
|
120dae4eed | ||
|
|
bb651db2d2 | ||
|
|
e929dde13e | ||
|
|
9d75385bbb | ||
|
|
1c526feec1 | ||
|
|
7df26986de | ||
|
|
5f2d23a12d | ||
|
|
d9e65c0969 | ||
|
|
ec1160924f | ||
|
|
230e8f895d | ||
|
|
af79378734 | ||
|
|
07ce5e0d22 | ||
|
|
9c8565cf21 | ||
|
|
5ad0ea2b5a | ||
|
|
e482053c8a | ||
|
|
945713d886 | ||
|
|
9bb62ad6b5 | ||
|
|
c2bda9fbde | ||
|
|
1d1db62a44 | ||
|
|
39405373f8 | ||
|
|
22a7988d3f | ||
|
|
b2092fafb7 | ||
|
|
cc7b5d8280 | ||
|
|
702d96a738 | ||
|
|
b9f34f1309 | ||
|
|
07724a0ddd | ||
|
|
83c3454685 | ||
|
|
7d263eb733 | ||
|
|
222687d9c5 | ||
|
|
07d3652e30 | ||
|
|
8d5b9d240a | ||
|
|
4f12eba944 | ||
|
|
a7f77d59c1 | ||
|
|
597248130f | ||
|
|
3c2c9cf317 | ||
|
|
e572b9d0cd | ||
|
|
52e9059a8d | ||
|
|
0cb9cff690 | ||
|
|
c0669cb2a5 | ||
|
|
c5902f2473 | ||
|
|
22028602e8 | ||
|
|
bd54608473 | ||
|
|
3741394269 | ||
|
|
6266d2df7e | ||
|
|
01dfba722a | ||
|
|
f8d5f01665 | ||
|
|
ad999d4791 | ||
|
|
6f1b258501 | ||
|
|
f949ddc0ab | ||
|
|
f53007cbf3 | ||
|
|
c287731df9 | ||
|
|
bc32c78d03 | ||
|
|
daee0db7bb | ||
|
|
91fbf4c79b | ||
|
|
54d9ef2f2a | ||
|
|
e056d4502b | ||
|
|
98c2c439aa | ||
|
|
b6068cea6b | ||
|
|
9c9affa719 | ||
|
|
8eb7dd0059 | ||
|
|
a62ad44883 | ||
|
|
2850354070 | ||
|
|
0a4abcbbc8 | ||
|
|
b491c350ae | ||
|
|
1fbe7c54bf | ||
|
|
9d32fc9bd1 | ||
|
|
542612129d | ||
|
|
750f87bb0a | ||
|
|
e168de79c7 | ||
|
|
9bca5a517f | ||
|
|
aa94cfb876 | ||
|
|
52b776b561 |
@@ -14,6 +14,7 @@ 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
|
||||||
@@ -48,6 +49,7 @@ 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_wireguard,with_utls,with_reality_server,with_clash_api,with_acme \
|
&& go build -v -trimpath -tags with_gvisor,with_quic,with_dhcp,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
|
||||||
|
|||||||
10
Makefile
10
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_wireguard,with_utls,with_reality_server,with_clash_api
|
TAGS ?= with_gvisor,with_quic,with_dhcp,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)
|
||||||
@@ -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 --rm-dist --snapshot || exit 1
|
go run ./cmd/internal/build goreleaser release --clean --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 --rm-dist --skip-publish || exit 1
|
go run ./cmd/internal/build goreleaser release --clean --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-20230413023804-244d7ff07035
|
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.0.0-20230728014906-3de089147f59
|
||||||
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.0.0-20230413023804-244d7ff07035
|
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.0.0-20230728014906-3de089147f59
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf bin dist sing-box
|
rm -rf bin dist sing-box
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ type ClashServer interface {
|
|||||||
PreStarter
|
PreStarter
|
||||||
Mode() string
|
Mode() string
|
||||||
StoreSelected() bool
|
StoreSelected() bool
|
||||||
|
StoreFakeIP() bool
|
||||||
CacheFile() ClashCacheFile
|
CacheFile() ClashCacheFile
|
||||||
HistoryStorage() *urltest.HistoryStorage
|
HistoryStorage() *urltest.HistoryStorage
|
||||||
RoutedConnection(ctx context.Context, conn net.Conn, metadata InboundContext, matchedRule Rule) (net.Conn, Tracker)
|
RoutedConnection(ctx context.Context, conn net.Conn, metadata InboundContext, matchedRule Rule) (net.Conn, Tracker)
|
||||||
@@ -22,6 +23,7 @@ type ClashServer interface {
|
|||||||
type ClashCacheFile interface {
|
type ClashCacheFile interface {
|
||||||
LoadSelected(group string) string
|
LoadSelected(group string) string
|
||||||
StoreSelected(group string, selected string) error
|
StoreSelected(group string, selected string) error
|
||||||
|
FakeIPStorage
|
||||||
}
|
}
|
||||||
|
|
||||||
type Tracker interface {
|
type Tracker interface {
|
||||||
@@ -29,10 +31,16 @@ type Tracker interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type OutboundGroup interface {
|
type OutboundGroup interface {
|
||||||
|
Outbound
|
||||||
Now() string
|
Now() string
|
||||||
All() []string
|
All() []string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type URLTestGroup interface {
|
||||||
|
OutboundGroup
|
||||||
|
URLTest(ctx context.Context, url string) (map[string]uint16, error)
|
||||||
|
}
|
||||||
|
|
||||||
func OutboundTag(detour Outbound) string {
|
func OutboundTag(detour Outbound) string {
|
||||||
if group, isGroup := detour.(OutboundGroup); isGroup {
|
if group, isGroup := detour.(OutboundGroup); isGroup {
|
||||||
return group.Now()
|
return group.Now()
|
||||||
|
|||||||
32
adapter/fakeip.go
Normal file
32
adapter/fakeip.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package adapter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/netip"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-dns"
|
||||||
|
"github.com/sagernet/sing/common/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FakeIPStore interface {
|
||||||
|
Service
|
||||||
|
Contains(address netip.Addr) bool
|
||||||
|
Create(domain string, isIPv6 bool) (netip.Addr, error)
|
||||||
|
Lookup(address netip.Addr) (string, bool)
|
||||||
|
Reset() error
|
||||||
|
}
|
||||||
|
|
||||||
|
type FakeIPStorage interface {
|
||||||
|
FakeIPMetadata() *FakeIPMetadata
|
||||||
|
FakeIPSaveMetadata(metadata *FakeIPMetadata) error
|
||||||
|
FakeIPSaveMetadataAsync(metadata *FakeIPMetadata)
|
||||||
|
FakeIPStore(address netip.Addr, domain string) error
|
||||||
|
FakeIPStoreAsync(address netip.Addr, domain string, logger logger.Logger)
|
||||||
|
FakeIPLoad(address netip.Addr) (string, bool)
|
||||||
|
FakeIPLoadDomain(domain string, isIPv6 bool) (netip.Addr, bool)
|
||||||
|
FakeIPReset() error
|
||||||
|
}
|
||||||
|
|
||||||
|
type FakeIPTransport interface {
|
||||||
|
dns.Transport
|
||||||
|
Store() FakeIPStore
|
||||||
|
}
|
||||||
50
adapter/fakeip_metadata.go
Normal file
50
adapter/fakeip_metadata.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package adapter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding"
|
||||||
|
"encoding/binary"
|
||||||
|
"io"
|
||||||
|
"net/netip"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FakeIPMetadata struct {
|
||||||
|
Inet4Range netip.Prefix
|
||||||
|
Inet6Range netip.Prefix
|
||||||
|
Inet4Current netip.Addr
|
||||||
|
Inet6Current netip.Addr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *FakeIPMetadata) MarshalBinary() (data []byte, err error) {
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
for _, marshaler := range []encoding.BinaryMarshaler{m.Inet4Range, m.Inet6Range, m.Inet4Current, m.Inet6Current} {
|
||||||
|
data, err = marshaler.MarshalBinary()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.Must(binary.Write(&buffer, binary.BigEndian, uint16(len(data))))
|
||||||
|
buffer.Write(data)
|
||||||
|
}
|
||||||
|
data = buffer.Bytes()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *FakeIPMetadata) UnmarshalBinary(data []byte) error {
|
||||||
|
reader := bytes.NewReader(data)
|
||||||
|
for _, unmarshaler := range []encoding.BinaryUnmarshaler{&m.Inet4Range, &m.Inet6Range, &m.Inet4Current, &m.Inet6Current} {
|
||||||
|
var length uint16
|
||||||
|
common.Must(binary.Read(reader, binary.BigEndian, &length))
|
||||||
|
element := make([]byte, length)
|
||||||
|
_, err := io.ReadFull(reader, element)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = unmarshaler.UnmarshalBinary(element)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -27,7 +27,7 @@ type InjectableInbound interface {
|
|||||||
type InboundContext struct {
|
type InboundContext struct {
|
||||||
Inbound string
|
Inbound string
|
||||||
InboundType string
|
InboundType string
|
||||||
IPVersion int
|
IPVersion uint8
|
||||||
Network string
|
Network string
|
||||||
Source M.Socksaddr
|
Source M.Socksaddr
|
||||||
Destination M.Socksaddr
|
Destination M.Socksaddr
|
||||||
@@ -46,6 +46,7 @@ type InboundContext struct {
|
|||||||
SourceGeoIPCode string
|
SourceGeoIPCode string
|
||||||
GeoIPCode string
|
GeoIPCode string
|
||||||
ProcessInfo *process.Info
|
ProcessInfo *process.Info
|
||||||
|
FakeIP bool
|
||||||
|
|
||||||
// dns cache
|
// dns cache
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ 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
|
||||||
|
|||||||
@@ -4,12 +4,6 @@ type PreStarter interface {
|
|||||||
PreStart() error
|
PreStart() error
|
||||||
}
|
}
|
||||||
|
|
||||||
func PreStart(starter any) error {
|
type PostStarter interface {
|
||||||
if preService, ok := starter.(PreStarter); ok {
|
PostStart() error
|
||||||
err := preService.PreStart()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ type Router interface {
|
|||||||
Outbound(tag string) (Outbound, bool)
|
Outbound(tag string) (Outbound, bool)
|
||||||
DefaultOutbound(network string) Outbound
|
DefaultOutbound(network string) Outbound
|
||||||
|
|
||||||
|
FakeIPStore() FakeIPStore
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
@@ -49,6 +51,8 @@ type Router interface {
|
|||||||
|
|
||||||
V2RayServer() V2RayServer
|
V2RayServer() V2RayServer
|
||||||
SetV2RayServer(server V2RayServer)
|
SetV2RayServer(server V2RayServer)
|
||||||
|
|
||||||
|
ResetNetwork() error
|
||||||
}
|
}
|
||||||
|
|
||||||
type routerContextKey struct{}
|
type routerContextKey struct{}
|
||||||
@@ -77,8 +81,9 @@ type Rule interface {
|
|||||||
type DNSRule interface {
|
type DNSRule interface {
|
||||||
Rule
|
Rule
|
||||||
DisableCache() bool
|
DisableCache() bool
|
||||||
|
RewriteTTL() *uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
type InterfaceUpdateListener interface {
|
type InterfaceUpdateListener interface {
|
||||||
InterfaceUpdated() error
|
InterfaceUpdated()
|
||||||
}
|
}
|
||||||
|
|||||||
47
box.go
47
box.go
@@ -19,6 +19,7 @@ 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)
|
||||||
@@ -46,12 +47,13 @@ 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 && experimentalOptions.ClashAPI.ExternalController != "" {
|
if experimentalOptions.ClashAPI != nil || options.PlatformInterface != nil {
|
||||||
needClashAPI = true
|
needClashAPI = true
|
||||||
}
|
}
|
||||||
if experimentalOptions.V2RayAPI != nil && experimentalOptions.V2RayAPI.Listen != "" {
|
if experimentalOptions.V2RayAPI != nil && experimentalOptions.V2RayAPI.Listen != "" {
|
||||||
@@ -62,6 +64,7 @@ func New(options Options) (*Box, error) {
|
|||||||
defaultLogWriter = io.Discard
|
defaultLogWriter = io.Discard
|
||||||
}
|
}
|
||||||
logFactory, err := log.New(log.Options{
|
logFactory, err := log.New(log.Options{
|
||||||
|
Context: ctx,
|
||||||
Options: common.PtrValueOrDefault(options.Log),
|
Options: common.PtrValueOrDefault(options.Log),
|
||||||
Observable: needClashAPI,
|
Observable: needClashAPI,
|
||||||
DefaultWriter: defaultLogWriter,
|
DefaultWriter: defaultLogWriter,
|
||||||
@@ -142,7 +145,7 @@ func New(options Options) (*Box, error) {
|
|||||||
preServices := make(map[string]adapter.Service)
|
preServices := make(map[string]adapter.Service)
|
||||||
postServices := make(map[string]adapter.Service)
|
postServices := make(map[string]adapter.Service)
|
||||||
if needClashAPI {
|
if needClashAPI {
|
||||||
clashServer, err := experimental.NewClashServer(router, logFactory.(log.ObservableFactory), common.PtrValueOrDefault(options.Experimental.ClashAPI))
|
clashServer, err := experimental.NewClashServer(ctx, router, logFactory.(log.ObservableFactory), common.PtrValueOrDefault(options.Experimental.ClashAPI))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, E.Cause(err, "create clash api server")
|
return nil, E.Cause(err, "create clash api server")
|
||||||
}
|
}
|
||||||
@@ -210,27 +213,18 @@ 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 {
|
||||||
s.logger.Trace("pre-start ", serviceName)
|
if preService, isPreService := service.(adapter.PreStarter); isPreService {
|
||||||
err := adapter.PreStart(service)
|
s.logger.Trace("pre-start ", serviceName)
|
||||||
if err != nil {
|
err := preService.PreStart()
|
||||||
return E.Cause(err, "pre-starting ", serviceName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for i, out := range s.outbounds {
|
|
||||||
var tag string
|
|
||||||
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 {
|
if err != nil {
|
||||||
return E.Cause(err, "initialize outbound/", out.Type(), "[", tag, "]")
|
return E.Cause(err, "pre-starting ", serviceName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
err := s.startOutbounds()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return s.router.Start()
|
return s.router.Start()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,13 +253,26 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
79
box_outbound.go
Normal file
79
box_outbound.go
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
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,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"go/build"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
|
||||||
@@ -11,6 +12,10 @@ 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,6 +5,7 @@ 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"
|
||||||
@@ -38,18 +39,24 @@ 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() {
|
||||||
@@ -70,9 +77,9 @@ func buildAndroid() {
|
|||||||
|
|
||||||
args = append(args, "-tags")
|
args = append(args, "-tags")
|
||||||
if !debugEnabled {
|
if !debugEnabled {
|
||||||
args = append(args, "with_gvisor,with_quic,with_wireguard,with_utls,with_clash_api")
|
args = append(args, strings.Join(sharedTags, ","))
|
||||||
} else {
|
} else {
|
||||||
args = append(args, "with_gvisor,with_quic,with_wireguard,with_utls,with_clash_api,debug")
|
args = append(args, strings.Join(append(sharedTags, debugTags...), ","))
|
||||||
}
|
}
|
||||||
args = append(args, "./experimental/libbox")
|
args = append(args, "./experimental/libbox")
|
||||||
|
|
||||||
@@ -100,7 +107,7 @@ func buildiOS() {
|
|||||||
args := []string{
|
args := []string{
|
||||||
"bind",
|
"bind",
|
||||||
"-v",
|
"-v",
|
||||||
"-target", "ios,iossimulator,macos",
|
"-target", "ios,iossimulator,tvos,tvossimulator,macos",
|
||||||
"-libname=box",
|
"-libname=box",
|
||||||
}
|
}
|
||||||
if !debugEnabled {
|
if !debugEnabled {
|
||||||
@@ -109,11 +116,12 @@ 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, "with_gvisor,with_quic,with_utls,with_clash_api,with_low_memory,with_conntrack")
|
args = append(args, strings.Join(tags, ","))
|
||||||
} else {
|
} else {
|
||||||
args = append(args, "with_gvisor,with_quic,with_utls,with_clash_api,with_low_memory,with_conntrack,debug")
|
args = append(args, strings.Join(append(tags, debugTags...), ","))
|
||||||
}
|
}
|
||||||
args = append(args, "./experimental/libbox")
|
args = append(args, "./experimental/libbox")
|
||||||
|
|
||||||
@@ -125,7 +133,7 @@ func buildiOS() {
|
|||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
copyPath := filepath.Join("..", "sing-box-for-ios")
|
copyPath := filepath.Join("..", "sing-box-for-apple")
|
||||||
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,6 +1,9 @@
|
|||||||
package build_shared
|
package build_shared
|
||||||
|
|
||||||
import "github.com/sagernet/sing/common/shell"
|
import (
|
||||||
|
"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()
|
||||||
@@ -12,5 +15,9 @@ 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()
|
||||||
return currentTagRev[1:] + "-" + shortCommit, nil
|
version := badversion.Parse(currentTagRev[1:])
|
||||||
|
if version.PreReleaseIdentifier == "" {
|
||||||
|
version.Patch++
|
||||||
|
}
|
||||||
|
return version.String() + "-" + shortCommit, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
//go:build debug
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
_ "net/http/pprof"
|
|
||||||
"runtime"
|
|
||||||
"runtime/debug"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/badjson"
|
|
||||||
"github.com/sagernet/sing-box/log"
|
|
||||||
|
|
||||||
"github.com/dustin/go-humanize"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
http.HandleFunc("/debug/gc", func(writer http.ResponseWriter, request *http.Request) {
|
|
||||||
writer.WriteHeader(http.StatusNoContent)
|
|
||||||
go debug.FreeOSMemory()
|
|
||||||
})
|
|
||||||
http.HandleFunc("/debug/memory", func(writer http.ResponseWriter, request *http.Request) {
|
|
||||||
var memStats runtime.MemStats
|
|
||||||
runtime.ReadMemStats(&memStats)
|
|
||||||
|
|
||||||
var memObject badjson.JSONObject
|
|
||||||
memObject.Put("heap", humanize.IBytes(memStats.HeapInuse))
|
|
||||||
memObject.Put("stack", humanize.IBytes(memStats.StackInuse))
|
|
||||||
memObject.Put("idle", humanize.IBytes(memStats.HeapIdle-memStats.HeapReleased))
|
|
||||||
memObject.Put("goroutines", runtime.NumGoroutine())
|
|
||||||
memObject.Put("rss", rusageMaxRSS())
|
|
||||||
|
|
||||||
encoder := json.NewEncoder(writer)
|
|
||||||
encoder.SetIndent("", " ")
|
|
||||||
encoder.Encode(memObject)
|
|
||||||
})
|
|
||||||
go func() {
|
|
||||||
err := http.ListenAndServe("0.0.0.0:8964", nil)
|
|
||||||
if err != nil {
|
|
||||||
log.Debug(err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
@@ -55,7 +55,7 @@ func WrapQUIC(err error) error {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if Contains(err, "canceled with error code 0") {
|
if Contains(err, "canceled by local with error code 0") {
|
||||||
return net.ErrClosed
|
return net.ErrClosed
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ func TestMergeJSON(t *testing.T) {
|
|||||||
{
|
{
|
||||||
Type: C.RuleTypeDefault,
|
Type: C.RuleTypeDefault,
|
||||||
DefaultOptions: option.DefaultRule{
|
DefaultOptions: option.DefaultRule{
|
||||||
Network: N.NetworkTCP,
|
Network: []string{N.NetworkTCP},
|
||||||
Outbound: "direct",
|
Outbound: "direct",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -42,7 +42,7 @@ func TestMergeJSON(t *testing.T) {
|
|||||||
{
|
{
|
||||||
Type: C.RuleTypeDefault,
|
Type: C.RuleTypeDefault,
|
||||||
DefaultOptions: option.DefaultRule{
|
DefaultOptions: option.DefaultRule{
|
||||||
Network: N.NetworkUDP,
|
Network: []string{N.NetworkUDP},
|
||||||
Outbound: "direct",
|
Outbound: "direct",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
120
common/badversion/version.go
Normal file
120
common/badversion/version.go
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
package badversion
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
F "github.com/sagernet/sing/common/format"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Version struct {
|
||||||
|
Major int
|
||||||
|
Minor int
|
||||||
|
Patch int
|
||||||
|
Commit string
|
||||||
|
PreReleaseIdentifier string
|
||||||
|
PreReleaseVersion int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Version) After(anotherVersion Version) bool {
|
||||||
|
if v.Major > anotherVersion.Major {
|
||||||
|
return true
|
||||||
|
} else if v.Major < anotherVersion.Major {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if v.Minor > anotherVersion.Minor {
|
||||||
|
return true
|
||||||
|
} else if v.Minor < anotherVersion.Minor {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if v.Patch > anotherVersion.Patch {
|
||||||
|
return true
|
||||||
|
} else if v.Patch < anotherVersion.Patch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if v.PreReleaseIdentifier == "" && anotherVersion.PreReleaseIdentifier != "" {
|
||||||
|
return true
|
||||||
|
} else if v.PreReleaseIdentifier != "" && anotherVersion.PreReleaseIdentifier == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if v.PreReleaseIdentifier != "" && anotherVersion.PreReleaseIdentifier != "" {
|
||||||
|
if v.PreReleaseIdentifier == anotherVersion.PreReleaseIdentifier {
|
||||||
|
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
|
||||||
|
} else if v.PreReleaseIdentifier == "alpha" && anotherVersion.PreReleaseIdentifier == "beta" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Version) String() string {
|
||||||
|
version := F.ToString(v.Major, ".", v.Minor, ".", v.Patch)
|
||||||
|
if v.PreReleaseIdentifier != "" {
|
||||||
|
version = F.ToString(version, "-", v.PreReleaseIdentifier, ".", v.PreReleaseVersion)
|
||||||
|
}
|
||||||
|
return version
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Version) BadString() string {
|
||||||
|
version := F.ToString(v.Major, ".", v.Minor)
|
||||||
|
if v.Patch > 0 {
|
||||||
|
version = F.ToString(version, ".", v.Patch)
|
||||||
|
}
|
||||||
|
if v.PreReleaseIdentifier != "" {
|
||||||
|
version = F.ToString(version, "-", v.PreReleaseIdentifier)
|
||||||
|
if v.PreReleaseVersion > 0 {
|
||||||
|
version = F.ToString(version, v.PreReleaseVersion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return version
|
||||||
|
}
|
||||||
|
|
||||||
|
func Parse(versionName string) (version Version) {
|
||||||
|
if strings.HasPrefix(versionName, "v") {
|
||||||
|
versionName = versionName[1:]
|
||||||
|
}
|
||||||
|
if strings.Contains(versionName, "-") {
|
||||||
|
parts := strings.Split(versionName, "-")
|
||||||
|
versionName = parts[0]
|
||||||
|
identifier := parts[1]
|
||||||
|
if strings.Contains(identifier, ".") {
|
||||||
|
identifierParts := strings.Split(identifier, ".")
|
||||||
|
version.PreReleaseIdentifier = identifierParts[0]
|
||||||
|
if len(identifierParts) >= 2 {
|
||||||
|
version.PreReleaseVersion, _ = strconv.Atoi(identifierParts[1])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if strings.HasPrefix(identifier, "alpha") {
|
||||||
|
version.PreReleaseIdentifier = "alpha"
|
||||||
|
version.PreReleaseVersion, _ = strconv.Atoi(identifier[5:])
|
||||||
|
} else if strings.HasPrefix(identifier, "beta") {
|
||||||
|
version.PreReleaseIdentifier = "beta"
|
||||||
|
version.PreReleaseVersion, _ = strconv.Atoi(identifier[4:])
|
||||||
|
} else {
|
||||||
|
version.Commit = identifier
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
versionElements := strings.Split(versionName, ".")
|
||||||
|
versionLen := len(versionElements)
|
||||||
|
if versionLen >= 1 {
|
||||||
|
version.Major, _ = strconv.Atoi(versionElements[0])
|
||||||
|
}
|
||||||
|
if versionLen >= 2 {
|
||||||
|
version.Minor, _ = strconv.Atoi(versionElements[1])
|
||||||
|
}
|
||||||
|
if versionLen >= 3 {
|
||||||
|
version.Patch, _ = strconv.Atoi(versionElements[2])
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
17
common/badversion/version_json.go
Normal file
17
common/badversion/version_json.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package badversion
|
||||||
|
|
||||||
|
import "github.com/sagernet/sing-box/common/json"
|
||||||
|
|
||||||
|
func (v Version) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(v.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Version) UnmarshalJSON(data []byte) error {
|
||||||
|
var version string
|
||||||
|
err := json.Unmarshal(data, &version)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*v = Parse(version)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
18
common/badversion/version_test.go
Normal file
18
common/badversion/version_test.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package badversion
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCompareVersion(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
require.Equal(t, "1.3.0-beta.1", Parse("v1.3.0-beta1").String())
|
||||||
|
require.Equal(t, "1.3-beta1", Parse("v1.3.0-beta.1").BadString())
|
||||||
|
require.True(t, Parse("1.3.0").After(Parse("1.3-beta1")))
|
||||||
|
require.True(t, Parse("1.3.0").After(Parse("1.3.0-beta1")))
|
||||||
|
require.True(t, Parse("1.3.0-beta1").After(Parse("1.3.0-alpha1")))
|
||||||
|
require.True(t, Parse("1.3.1").After(Parse("1.3.0")))
|
||||||
|
require.True(t, Parse("1.4").After(Parse("1.3")))
|
||||||
|
}
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
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,6 +4,7 @@ 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"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -42,7 +43,7 @@ func (c *PacketConn) Close() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *PacketConn) Upstream() any {
|
func (c *PacketConn) Upstream() any {
|
||||||
return c.PacketConn
|
return bufio.NewPacketConn(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 {
|
func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDialer, error) {
|
||||||
var dialer net.Dialer
|
var dialer net.Dialer
|
||||||
var listener net.ListenConfig
|
var listener net.ListenConfig
|
||||||
if options.BindInterface != "" {
|
if options.BindInterface != "" {
|
||||||
@@ -93,6 +93,12 @@ func NewDefault(router adapter.Router, options option.DialerOptions) *DefaultDia
|
|||||||
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},
|
||||||
@@ -101,7 +107,7 @@ func NewDefault(router adapter.Router, options option.DialerOptions) *DefaultDia
|
|||||||
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) {
|
||||||
|
|||||||
11
common/dialer/default_go1.21.go
Normal file
11
common/dialer/default_go1.21.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
//go:build go1.21
|
||||||
|
|
||||||
|
package dialer
|
||||||
|
|
||||||
|
import "net"
|
||||||
|
|
||||||
|
const multipathTCPAvailable = true
|
||||||
|
|
||||||
|
func setMultiPathTCP(dialer *net.Dialer) {
|
||||||
|
dialer.SetMultipathTCP(true)
|
||||||
|
}
|
||||||
12
common/dialer/default_nongo1.21.go
Normal file
12
common/dialer/default_nongo1.21.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
//go:build !go1.21
|
||||||
|
|
||||||
|
package dialer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
const multipathTCPAvailable = false
|
||||||
|
|
||||||
|
func setMultiPathTCP(dialer *net.Dialer) {
|
||||||
|
}
|
||||||
@@ -6,13 +6,24 @@ 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 New(router adapter.Router, options option.DialerOptions) N.Dialer {
|
func MustNew(router adapter.Router, options option.DialerOptions) N.Dialer {
|
||||||
var dialer N.Dialer
|
return common.Must1(New(router, options))
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(router adapter.Router, options option.DialerOptions) (N.Dialer, error) {
|
||||||
|
var (
|
||||||
|
dialer N.Dialer
|
||||||
|
err error
|
||||||
|
)
|
||||||
if options.Detour == "" {
|
if options.Detour == "" {
|
||||||
dialer = NewDefault(router, options)
|
dialer, err = NewDefault(router, options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
dialer = NewDetour(router, options.Detour)
|
dialer = NewDetour(router, options.Detour)
|
||||||
}
|
}
|
||||||
@@ -20,5 +31,5 @@ func New(router adapter.Router, options option.DialerOptions) N.Dialer {
|
|||||||
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
|
return dialer, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,12 @@ type slowOpenConn struct {
|
|||||||
|
|
||||||
func DialSlowContext(dialer *tfo.Dialer, ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
func DialSlowContext(dialer *tfo.Dialer, ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||||
if dialer.DisableTFO || N.NetworkName(network) != N.NetworkTCP {
|
if dialer.DisableTFO || N.NetworkName(network) != N.NetworkTCP {
|
||||||
return dialer.DialContext(ctx, network, destination.String(), nil)
|
switch N.NetworkName(network) {
|
||||||
|
case N.NetworkTCP, N.NetworkUDP:
|
||||||
|
return dialer.Dialer.DialContext(ctx, network, destination.String())
|
||||||
|
default:
|
||||||
|
return dialer.Dialer.DialContext(ctx, network, destination.AddrString())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return &slowOpenConn{
|
return &slowOpenConn{
|
||||||
dialer: dialer,
|
dialer: dialer,
|
||||||
@@ -123,13 +128,6 @@ 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,535 +1,21 @@
|
|||||||
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/common"
|
"github.com/sagernet/sing-mux"
|
||||||
"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"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ N.Dialer = (*Client)(nil)
|
func NewClientWithOptions(dialer N.Dialer, options option.MultiplexOptions) (*Client, error) {
|
||||||
|
|
||||||
type Client struct {
|
|
||||||
access sync.Mutex
|
|
||||||
connections list.List[abstractSession]
|
|
||||||
ctx context.Context
|
|
||||||
dialer N.Dialer
|
|
||||||
protocol Protocol
|
|
||||||
maxConnections int
|
|
||||||
minStreams int
|
|
||||||
maxStreams int
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewClient(ctx context.Context, dialer N.Dialer, protocol Protocol, maxConnections int, minStreams int, maxStreams int) *Client {
|
|
||||||
return &Client{
|
|
||||||
ctx: ctx,
|
|
||||||
dialer: dialer,
|
|
||||||
protocol: protocol,
|
|
||||||
maxConnections: maxConnections,
|
|
||||||
minStreams: minStreams,
|
|
||||||
maxStreams: maxStreams,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewClientWithOptions(ctx context.Context, dialer N.Dialer, options option.MultiplexOptions) (N.Dialer, error) {
|
|
||||||
if !options.Enabled {
|
if !options.Enabled {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
if options.MaxConnections == 0 && options.MaxStreams == 0 {
|
return mux.NewClient(mux.Options{
|
||||||
options.MinStreams = 8
|
Dialer: dialer,
|
||||||
}
|
Protocol: options.Protocol,
|
||||||
protocol, err := ParseProtocol(options.Protocol)
|
MaxConnections: options.MaxConnections,
|
||||||
if err != nil {
|
MinStreams: options.MinStreams,
|
||||||
return nil, err
|
MaxStreams: options.MaxStreams,
|
||||||
}
|
Padding: options.Padding,
|
||||||
return NewClient(ctx, dialer, protocol, options.MaxConnections, options.MinStreams, options.MaxStreams), nil
|
})
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
|
||||||
switch N.NetworkName(network) {
|
|
||||||
case N.NetworkTCP:
|
|
||||||
stream, err := c.openStream()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &ClientConn{Conn: stream, destination: destination}, nil
|
|
||||||
case N.NetworkUDP:
|
|
||||||
stream, err := c.openStream()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return bufio.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()
|
|
||||||
}
|
|
||||||
sLen := len(sessions)
|
|
||||||
if sLen == 0 {
|
|
||||||
return c.offerNew()
|
|
||||||
}
|
|
||||||
session := common.MinBy(sessions, abstractSession.NumStreams)
|
|
||||||
numStreams := session.NumStreams()
|
|
||||||
if numStreams == 0 {
|
|
||||||
return session, nil
|
|
||||||
}
|
|
||||||
if c.maxConnections > 0 {
|
|
||||||
if sLen >= c.maxConnections || numStreams < c.minStreams {
|
|
||||||
return session, nil
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if c.maxStreams > 0 && numStreams < c.maxStreams {
|
|
||||||
return session, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return c.offerNew()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) offerNew() (abstractSession, error) {
|
|
||||||
conn, err := c.dialer.DialContext(c.ctx, N.NetworkTCP, Destination)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if vectorisedWriter, isVectorised := bufio.CreateVectorisedWriter(conn); isVectorised {
|
|
||||||
conn = &vectorisedProtocolConn{protocolConn{Conn: conn, protocol: c.protocol}, vectorisedWriter}
|
|
||||||
} else {
|
|
||||||
conn = &protocolConn{Conn: conn, protocol: c.protocol}
|
|
||||||
}
|
|
||||||
session, err := c.protocol.newClient(conn)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
c.connections.PushBack(session)
|
|
||||||
return session, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) Close() error {
|
|
||||||
c.access.Lock()
|
|
||||||
defer c.access.Unlock()
|
|
||||||
for _, session := range c.connections.Array() {
|
|
||||||
session.Close()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type ClientConn struct {
|
|
||||||
net.Conn
|
|
||||||
destination M.Socksaddr
|
|
||||||
requestWrite bool
|
|
||||||
responseRead bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ClientConn) readResponse() error {
|
|
||||||
response, err := ReadStreamResponse(c.Conn)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if response.Status == statusError {
|
|
||||||
return E.New("remote error: ", response.Message)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ClientConn) Read(b []byte) (n int, err error) {
|
|
||||||
if !c.responseRead {
|
|
||||||
err = c.readResponse()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.responseRead = true
|
|
||||||
}
|
|
||||||
return c.Conn.Read(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ClientConn) Write(b []byte) (n int, err error) {
|
|
||||||
if c.requestWrite {
|
|
||||||
return c.Conn.Write(b)
|
|
||||||
}
|
|
||||||
request := StreamRequest{
|
|
||||||
Network: N.NetworkTCP,
|
|
||||||
Destination: c.destination,
|
|
||||||
}
|
|
||||||
_buffer := buf.StackNewSize(requestLen(request) + len(b))
|
|
||||||
defer common.KeepAlive(_buffer)
|
|
||||||
buffer := common.Dup(_buffer)
|
|
||||||
defer buffer.Release()
|
|
||||||
EncodeStreamRequest(request, buffer)
|
|
||||||
buffer.Write(b)
|
|
||||||
_, err = c.Conn.Write(buffer.Bytes())
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.requestWrite = true
|
|
||||||
return len(b), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ClientConn) ReadFrom(r io.Reader) (n int64, err error) {
|
|
||||||
if !c.requestWrite {
|
|
||||||
return bufio.ReadFrom0(c, r)
|
|
||||||
}
|
|
||||||
return bufio.Copy(c.Conn, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ClientConn) WriteTo(w io.Writer) (n int64, err error) {
|
|
||||||
if !c.responseRead {
|
|
||||||
return bufio.WriteTo0(c, w)
|
|
||||||
}
|
|
||||||
return bufio.Copy(w, c.Conn)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ClientConn) LocalAddr() net.Addr {
|
|
||||||
return c.Conn.LocalAddr()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ClientConn) RemoteAddr() net.Addr {
|
|
||||||
return c.destination.TCPAddr()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ClientConn) ReaderReplaceable() bool {
|
|
||||||
return c.responseRead
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ClientConn) WriterReplaceable() bool {
|
|
||||||
return c.requestWrite
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ClientConn) 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 := requestLen(request)
|
|
||||||
if len(payload) > 0 {
|
|
||||||
rLen += 2 + len(payload)
|
|
||||||
}
|
|
||||||
_buffer := buf.StackNewSize(rLen)
|
|
||||||
defer common.KeepAlive(_buffer)
|
|
||||||
buffer := common.Dup(_buffer)
|
|
||||||
defer buffer.Release()
|
|
||||||
EncodeStreamRequest(request, buffer)
|
|
||||||
if len(payload) > 0 {
|
|
||||||
common.Must(
|
|
||||||
binary.Write(buffer, binary.BigEndian, uint16(len(payload))),
|
|
||||||
common.Error(buffer.Write(payload)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
_, err = c.ExtendedConn.Write(buffer.Bytes())
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.requestWrite = true
|
|
||||||
return len(payload), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ClientPacketConn) Write(b []byte) (n int, err error) {
|
|
||||||
if !c.requestWrite {
|
|
||||||
return c.writeRequest(b)
|
|
||||||
}
|
|
||||||
err = binary.Write(c.ExtendedConn, binary.BigEndian, uint16(len(b)))
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return c.ExtendedConn.Write(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ClientPacketConn) ReadBuffer(buffer *buf.Buffer) (err error) {
|
|
||||||
if !c.responseRead {
|
|
||||||
err = c.readResponse()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.responseRead = true
|
|
||||||
}
|
|
||||||
var length uint16
|
|
||||||
err = binary.Read(c.ExtendedConn, binary.BigEndian, &length)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
_, err = buffer.ReadFullFrom(c.ExtendedConn, int(length))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ClientPacketConn) WriteBuffer(buffer *buf.Buffer) error {
|
|
||||||
if !c.requestWrite {
|
|
||||||
defer buffer.Release()
|
|
||||||
return common.Error(c.writeRequest(buffer.Bytes()))
|
|
||||||
}
|
|
||||||
bLen := buffer.Len()
|
|
||||||
binary.BigEndian.PutUint16(buffer.ExtendHeader(2), uint16(bLen))
|
|
||||||
return c.ExtendedConn.WriteBuffer(buffer)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ClientPacketConn) FrontHeadroom() int {
|
|
||||||
return 2
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ClientPacketConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) {
|
|
||||||
err = c.ReadBuffer(buffer)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ClientPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
|
|
||||||
return c.WriteBuffer(buffer)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ClientPacketConn) LocalAddr() net.Addr {
|
|
||||||
return c.ExtendedConn.LocalAddr()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ClientPacketConn) RemoteAddr() net.Addr {
|
|
||||||
return c.destination.UDPAddr()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ClientPacketConn) 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 := requestLen(request)
|
|
||||||
if len(payload) > 0 {
|
|
||||||
rLen += M.SocksaddrSerializer.AddrPortLen(destination) + 2 + len(payload)
|
|
||||||
}
|
|
||||||
_buffer := buf.StackNewSize(rLen)
|
|
||||||
defer common.KeepAlive(_buffer)
|
|
||||||
buffer := common.Dup(_buffer)
|
|
||||||
defer buffer.Release()
|
|
||||||
EncodeStreamRequest(request, buffer)
|
|
||||||
if len(payload) > 0 {
|
|
||||||
common.Must(
|
|
||||||
M.SocksaddrSerializer.WriteAddrPort(buffer, destination),
|
|
||||||
binary.Write(buffer, binary.BigEndian, uint16(len(payload))),
|
|
||||||
common.Error(buffer.Write(payload)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
_, err = c.ExtendedConn.Write(buffer.Bytes())
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.requestWrite = true
|
|
||||||
return len(payload), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ClientPacketAddrConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
|
|
||||||
if !c.requestWrite {
|
|
||||||
return c.writeRequest(p, M.SocksaddrFromNet(addr))
|
|
||||||
}
|
|
||||||
err = M.SocksaddrSerializer.WriteAddrPort(c.ExtendedConn, M.SocksaddrFromNet(addr))
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err = binary.Write(c.ExtendedConn, binary.BigEndian, uint16(len(p)))
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return c.ExtendedConn.Write(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ClientPacketAddrConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) {
|
|
||||||
if !c.responseRead {
|
|
||||||
err = c.readResponse()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.responseRead = true
|
|
||||||
}
|
|
||||||
destination, err = M.SocksaddrSerializer.ReadAddrPort(c.ExtendedConn)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var length uint16
|
|
||||||
err = binary.Read(c.ExtendedConn, binary.BigEndian, &length)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
_, err = buffer.ReadFullFrom(c.ExtendedConn, int(length))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ClientPacketAddrConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
|
|
||||||
if !c.requestWrite {
|
|
||||||
defer buffer.Release()
|
|
||||||
return common.Error(c.writeRequest(buffer.Bytes(), destination))
|
|
||||||
}
|
|
||||||
bLen := buffer.Len()
|
|
||||||
header := buf.With(buffer.ExtendHeader(M.SocksaddrSerializer.AddrPortLen(destination) + 2))
|
|
||||||
common.Must(
|
|
||||||
M.SocksaddrSerializer.WriteAddrPort(header, destination),
|
|
||||||
binary.Write(header, binary.BigEndian, uint16(bLen)),
|
|
||||||
)
|
|
||||||
return c.ExtendedConn.WriteBuffer(buffer)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ClientPacketAddrConn) LocalAddr() net.Addr {
|
|
||||||
return c.ExtendedConn.LocalAddr()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ClientPacketAddrConn) FrontHeadroom() int {
|
|
||||||
return 2 + M.MaxSocksaddrLength
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ClientPacketAddrConn) NeedAdditionalReadDeadline() bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ClientPacketAddrConn) Upstream() any {
|
|
||||||
return c.ExtendedConn
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,240 +1,14 @@
|
|||||||
package mux
|
package mux
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/binary"
|
"github.com/sagernet/sing-mux"
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
|
||||||
"github.com/sagernet/sing/common"
|
|
||||||
"github.com/sagernet/sing/common/buf"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
"github.com/sagernet/sing/common/rw"
|
|
||||||
"github.com/sagernet/smux"
|
|
||||||
|
|
||||||
"github.com/hashicorp/yamux"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var Destination = M.Socksaddr{
|
type (
|
||||||
Fqdn: "sp.mux.sing-box.arpa",
|
Client = mux.Client
|
||||||
Port: 444,
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
ProtocolSMux Protocol = iota
|
|
||||||
ProtocolYAMux
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Protocol byte
|
var (
|
||||||
|
Destination = mux.Destination
|
||||||
func ParseProtocol(name string) (Protocol, error) {
|
HandleConnection = mux.HandleConnection
|
||||||
switch name {
|
|
||||||
case "", "smux":
|
|
||||||
return ProtocolSMux, nil
|
|
||||||
case "yamux":
|
|
||||||
return ProtocolYAMux, nil
|
|
||||||
default:
|
|
||||||
return ProtocolYAMux, E.New("unknown multiplex protocol: ", name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p Protocol) newServer(conn net.Conn) (abstractSession, error) {
|
|
||||||
switch p {
|
|
||||||
case ProtocolSMux:
|
|
||||||
session, err := smux.Server(conn, smuxConfig())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &smuxSession{session}, nil
|
|
||||||
case ProtocolYAMux:
|
|
||||||
return yamux.Server(conn, yaMuxConfig())
|
|
||||||
default:
|
|
||||||
panic("unknown protocol")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p Protocol) newClient(conn net.Conn) (abstractSession, error) {
|
|
||||||
switch p {
|
|
||||||
case ProtocolSMux:
|
|
||||||
session, err := smux.Client(conn, smuxConfig())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &smuxSession{session}, nil
|
|
||||||
case ProtocolYAMux:
|
|
||||||
return yamux.Client(conn, yaMuxConfig())
|
|
||||||
default:
|
|
||||||
panic("unknown protocol")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func smuxConfig() *smux.Config {
|
|
||||||
config := smux.DefaultConfig()
|
|
||||||
config.KeepAliveDisabled = true
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
func yaMuxConfig() *yamux.Config {
|
|
||||||
config := yamux.DefaultConfig()
|
|
||||||
config.LogOutput = io.Discard
|
|
||||||
config.StreamCloseTimeout = C.TCPTimeout
|
|
||||||
config.StreamOpenTimeout = C.TCPTimeout
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p Protocol) String() string {
|
|
||||||
switch p {
|
|
||||||
case ProtocolSMux:
|
|
||||||
return "smux"
|
|
||||||
case ProtocolYAMux:
|
|
||||||
return "yamux"
|
|
||||||
default:
|
|
||||||
return "unknown"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
version0 = 0
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Request struct {
|
|
||||||
Protocol Protocol
|
|
||||||
}
|
|
||||||
|
|
||||||
func ReadRequest(reader io.Reader) (*Request, error) {
|
|
||||||
version, err := rw.ReadByte(reader)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if version != version0 {
|
|
||||||
return nil, E.New("unsupported version: ", version)
|
|
||||||
}
|
|
||||||
protocol, err := rw.ReadByte(reader)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if protocol > byte(ProtocolYAMux) {
|
|
||||||
return nil, E.New("unsupported protocol: ", protocol)
|
|
||||||
}
|
|
||||||
return &Request{Protocol: Protocol(protocol)}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func EncodeRequest(buffer *buf.Buffer, request Request) {
|
|
||||||
buffer.WriteByte(version0)
|
|
||||||
buffer.WriteByte(byte(request.Protocol))
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
flagUDP = 1
|
|
||||||
flagAddr = 2
|
|
||||||
statusSuccess = 0
|
|
||||||
statusError = 1
|
|
||||||
)
|
|
||||||
|
|
||||||
type StreamRequest struct {
|
|
||||||
Network string
|
|
||||||
Destination M.Socksaddr
|
|
||||||
PacketAddr bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func ReadStreamRequest(reader io.Reader) (*StreamRequest, error) {
|
|
||||||
var flags uint16
|
|
||||||
err := binary.Read(reader, binary.BigEndian, &flags)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
destination, err := M.SocksaddrSerializer.ReadAddrPort(reader)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var network string
|
|
||||||
var udpAddr bool
|
|
||||||
if flags&flagUDP == 0 {
|
|
||||||
network = N.NetworkTCP
|
|
||||||
} else {
|
|
||||||
network = N.NetworkUDP
|
|
||||||
udpAddr = flags&flagAddr != 0
|
|
||||||
}
|
|
||||||
return &StreamRequest{network, destination, udpAddr}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func requestLen(request StreamRequest) int {
|
|
||||||
var rLen int
|
|
||||||
rLen += 1 // version
|
|
||||||
rLen += 2 // flags
|
|
||||||
rLen += M.SocksaddrSerializer.AddrPortLen(request.Destination)
|
|
||||||
return rLen
|
|
||||||
}
|
|
||||||
|
|
||||||
func EncodeStreamRequest(request StreamRequest, buffer *buf.Buffer) {
|
|
||||||
destination := request.Destination
|
|
||||||
var flags uint16
|
|
||||||
if request.Network == N.NetworkUDP {
|
|
||||||
flags |= flagUDP
|
|
||||||
}
|
|
||||||
if request.PacketAddr {
|
|
||||||
flags |= flagAddr
|
|
||||||
if !destination.IsValid() {
|
|
||||||
destination = Destination
|
|
||||||
}
|
|
||||||
}
|
|
||||||
common.Must(
|
|
||||||
binary.Write(buffer, binary.BigEndian, flags),
|
|
||||||
M.SocksaddrSerializer.WriteAddrPort(buffer, destination),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
type StreamResponse struct {
|
|
||||||
Status uint8
|
|
||||||
Message string
|
|
||||||
}
|
|
||||||
|
|
||||||
func ReadStreamResponse(reader io.Reader) (*StreamResponse, error) {
|
|
||||||
var response StreamResponse
|
|
||||||
status, err := rw.ReadByte(reader)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
response.Status = status
|
|
||||||
if status == statusError {
|
|
||||||
response.Message, err = rw.ReadVString(reader)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &response, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type wrapStream struct {
|
|
||||||
net.Conn
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *wrapStream) Read(p []byte) (n int, err error) {
|
|
||||||
n, err = w.Conn.Read(p)
|
|
||||||
err = wrapError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *wrapStream) Write(p []byte) (n int, err error) {
|
|
||||||
n, err = w.Conn.Write(p)
|
|
||||||
err = wrapError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *wrapStream) WriteIsThreadUnsafe() {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *wrapStream) Upstream() any {
|
|
||||||
return w.Conn
|
|
||||||
}
|
|
||||||
|
|
||||||
func wrapError(err error) error {
|
|
||||||
switch err {
|
|
||||||
case yamux.ErrStreamClosed:
|
|
||||||
return io.EOF
|
|
||||||
default:
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,269 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
package mux
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing/common"
|
|
||||||
"github.com/sagernet/sing/common/buf"
|
|
||||||
"github.com/sagernet/sing/common/bufio"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
"github.com/sagernet/smux"
|
|
||||||
)
|
|
||||||
|
|
||||||
type abstractSession interface {
|
|
||||||
Open() (net.Conn, error)
|
|
||||||
Accept() (net.Conn, error)
|
|
||||||
NumStreams() int
|
|
||||||
Close() error
|
|
||||||
IsClosed() bool
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ abstractSession = (*smuxSession)(nil)
|
|
||||||
|
|
||||||
type smuxSession struct {
|
|
||||||
*smux.Session
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *smuxSession) Open() (net.Conn, error) {
|
|
||||||
return s.OpenStream()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *smuxSession) Accept() (net.Conn, error) {
|
|
||||||
return s.AcceptStream()
|
|
||||||
}
|
|
||||||
|
|
||||||
type protocolConn struct {
|
|
||||||
net.Conn
|
|
||||||
protocol Protocol
|
|
||||||
protocolWritten bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *protocolConn) Write(p []byte) (n int, err error) {
|
|
||||||
if c.protocolWritten {
|
|
||||||
return c.Conn.Write(p)
|
|
||||||
}
|
|
||||||
_buffer := buf.StackNewSize(2 + len(p))
|
|
||||||
defer common.KeepAlive(_buffer)
|
|
||||||
buffer := common.Dup(_buffer)
|
|
||||||
defer buffer.Release()
|
|
||||||
EncodeRequest(buffer, Request{
|
|
||||||
Protocol: c.protocol,
|
|
||||||
})
|
|
||||||
common.Must(common.Error(buffer.Write(p)))
|
|
||||||
n, err = c.Conn.Write(buffer.Bytes())
|
|
||||||
if err == nil {
|
|
||||||
n--
|
|
||||||
}
|
|
||||||
c.protocolWritten = true
|
|
||||||
return n, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *protocolConn) ReadFrom(r io.Reader) (n int64, err error) {
|
|
||||||
if !c.protocolWritten {
|
|
||||||
return bufio.ReadFrom0(c, r)
|
|
||||||
}
|
|
||||||
return bufio.Copy(c.Conn, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *protocolConn) Upstream() any {
|
|
||||||
return c.Conn
|
|
||||||
}
|
|
||||||
|
|
||||||
type vectorisedProtocolConn struct {
|
|
||||||
protocolConn
|
|
||||||
N.VectorisedWriter
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *vectorisedProtocolConn) WriteVectorised(buffers []*buf.Buffer) error {
|
|
||||||
if c.protocolWritten {
|
|
||||||
return c.VectorisedWriter.WriteVectorised(buffers)
|
|
||||||
}
|
|
||||||
c.protocolWritten = true
|
|
||||||
_buffer := buf.StackNewSize(2)
|
|
||||||
defer common.KeepAlive(_buffer)
|
|
||||||
buffer := common.Dup(_buffer)
|
|
||||||
defer buffer.Release()
|
|
||||||
EncodeRequest(buffer, Request{
|
|
||||||
Protocol: c.protocol,
|
|
||||||
})
|
|
||||||
return c.VectorisedWriter.WriteVectorised(append([]*buf.Buffer{buffer}, buffers...))
|
|
||||||
}
|
|
||||||
@@ -15,7 +15,6 @@ 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"
|
||||||
@@ -82,9 +81,7 @@ 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.StackNew()
|
buffer := buf.New()
|
||||||
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) error {
|
func (p *systemProxy) update(event int) {
|
||||||
newInterfaceName := p.monitor.DefaultInterfaceName(netip.IPv4Unspecified())
|
newInterfaceName := p.monitor.DefaultInterfaceName(netip.IPv4Unspecified())
|
||||||
if p.interfaceName == newInterfaceName {
|
if p.interfaceName == newInterfaceName {
|
||||||
return nil
|
return
|
||||||
}
|
}
|
||||||
if p.interfaceName != "" {
|
if p.interfaceName != "" {
|
||||||
_ = p.unset()
|
_ = p.unset()
|
||||||
@@ -31,7 +31,7 @@ func (p *systemProxy) update(event int) error {
|
|||||||
p.interfaceName = newInterfaceName
|
p.interfaceName = newInterfaceName
|
||||||
interfaceDisplayName, err := getInterfaceDisplayName(p.interfaceName)
|
interfaceDisplayName, err := getInterfaceDisplayName(p.interfaceName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return
|
||||||
}
|
}
|
||||||
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) error {
|
|||||||
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 {
|
||||||
err = shell.Exec("networksetup", "-setsecurewebproxy", interfaceDisplayName, "127.0.0.1", F.ToString(p.port)).Attach().Run()
|
_ = shell.Exec("networksetup", "-setsecurewebproxy", interfaceDisplayName, "127.0.0.1", F.ToString(p.port)).Attach().Run()
|
||||||
}
|
}
|
||||||
return err
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *systemProxy) unset() error {
|
func (p *systemProxy) unset() error {
|
||||||
@@ -88,10 +88,7 @@ func SetSystemProxy(router adapter.Router, port uint16, isMixed bool) (func() er
|
|||||||
port: port,
|
port: port,
|
||||||
isMixed: isMixed,
|
isMixed: isMixed,
|
||||||
}
|
}
|
||||||
err := proxy.update(tun.EventInterfaceUpdate)
|
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,9 +26,7 @@ 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.StackNewSize(int(length))
|
buffer := buf.NewSize(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,6 +21,7 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,7 +30,7 @@ func (w *acmeWrapper) Start() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (w *acmeWrapper) Close() error {
|
func (w *acmeWrapper) Close() error {
|
||||||
w.cfg.Unmanage(w.domain)
|
w.cache.Stop()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,10 +78,11 @@ 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)}
|
||||||
config = certmagic.New(certmagic.NewCache(certmagic.CacheOptions{
|
cache := 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)
|
})
|
||||||
return config.TLSConfig(), &acmeWrapper{ctx, config, options.Domain}, nil
|
config = certmagic.New(cache, *config)
|
||||||
|
return config.TLSConfig(), &acmeWrapper{ctx: ctx, cfg: config, cache: cache, domain: options.Domain}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ func (e *RealityClientConfig) ClientHandshake(ctx context.Context, conn net.Conn
|
|||||||
|
|
||||||
hello.SessionId[0] = 1
|
hello.SessionId[0] = 1
|
||||||
hello.SessionId[1] = 8
|
hello.SessionId[1] = 8
|
||||||
hello.SessionId[2] = 0
|
hello.SessionId[2] = 1
|
||||||
binary.BigEndian.PutUint32(hello.SessionId[4:], uint32(time.Now().Unix()))
|
binary.BigEndian.PutUint32(hello.SessionId[4:], uint32(time.Now().Unix()))
|
||||||
copy(hello.SessionId[8:], e.shortID[:])
|
copy(hello.SessionId[8:], e.shortID[:])
|
||||||
|
|
||||||
|
|||||||
@@ -101,7 +101,10 @@ func NewRealityServer(ctx context.Context, router adapter.Router, logger log.Log
|
|||||||
tlsConfig.ShortIds[shortID] = true
|
tlsConfig.ShortIds[shortID] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
handshakeDialer := dialer.New(router, options.Reality.Handshake.DialerOptions)
|
handshakeDialer, err := 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 {
|
||||||
tlsConfig, acmeService, err = startACME(ctx, common.PtrValueOrDefault(options.ACME))
|
|
||||||
//nolint:staticcheck
|
//nolint:staticcheck
|
||||||
|
tlsConfig, acmeService, err = startACME(ctx, common.PtrValueOrDefault(options.ACME))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ 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 {
|
||||||
@@ -20,6 +21,7 @@ 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 {
|
||||||
@@ -28,6 +30,18 @@ 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
|
||||||
@@ -39,17 +53,30 @@ func (s *HistoryStorage) LoadURLTestHistory(tag string) *History {
|
|||||||
|
|
||||||
func (s *HistoryStorage) DeleteURLTestHistory(tag string) {
|
func (s *HistoryStorage) DeleteURLTestHistory(tag string) {
|
||||||
s.access.Lock()
|
s.access.Lock()
|
||||||
defer s.access.Unlock()
|
|
||||||
delete(s.delayHistory, tag)
|
delete(s.delayHistory, tag)
|
||||||
|
s.access.Unlock()
|
||||||
|
s.notifyUpdated()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *HistoryStorage) StoreURLTestHistory(tag string, history *History) {
|
func (s *HistoryStorage) StoreURLTestHistory(tag string, history *History) {
|
||||||
s.access.Lock()
|
s.access.Lock()
|
||||||
defer s.access.Unlock()
|
|
||||||
s.delayHistory[tag] = history
|
s.delayHistory[tag] = history
|
||||||
|
s.access.Unlock()
|
||||||
|
s.notifyUpdated()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *HistoryStorage) notifyUpdated() {
|
||||||
|
s.access.RLock()
|
||||||
|
defer s.access.RUnlock()
|
||||||
|
for element := s.callbacks.Front(); element != nil; element = element.Next() {
|
||||||
|
element.Value()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func URLTest(ctx context.Context, link string, detour N.Dialer) (t uint16, err error) {
|
func URLTest(ctx context.Context, link string, detour N.Dialer) (t uint16, err error) {
|
||||||
|
if link == "" {
|
||||||
|
link = "https://www.gstatic.com/generate_204"
|
||||||
|
}
|
||||||
linkURL, err := url.Parse(link)
|
linkURL, err := url.Parse(link)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -3,28 +3,13 @@ package constant
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing/common/rw"
|
"github.com/sagernet/sing/common/rw"
|
||||||
)
|
)
|
||||||
|
|
||||||
const dirName = "sing-box"
|
const dirName = "sing-box"
|
||||||
|
|
||||||
var (
|
var resourcePaths []string
|
||||||
basePath string
|
|
||||||
resourcePaths []string
|
|
||||||
)
|
|
||||||
|
|
||||||
func BasePath(name string) string {
|
|
||||||
if basePath == "" || strings.HasPrefix(name, "/") {
|
|
||||||
return name
|
|
||||||
}
|
|
||||||
return filepath.Join(basePath, name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func SetBasePath(path string) {
|
|
||||||
basePath = path
|
|
||||||
}
|
|
||||||
|
|
||||||
func FindPath(name string) (string, bool) {
|
func FindPath(name string) (string, bool) {
|
||||||
name = os.ExpandEnv(name)
|
name = os.ExpandEnv(name)
|
||||||
|
|||||||
@@ -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,9 +21,55 @@ 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func applyDebugOptions(options option.DebugOptions) {
|
func applyDebugOptions(options option.DebugOptions) {
|
||||||
|
applyDebugListenOption(options)
|
||||||
if options.GCPercent != nil {
|
if options.GCPercent != nil {
|
||||||
debug.SetGCPercent(*options.GCPercent)
|
debug.SetGCPercent(*options.GCPercent)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func applyDebugOptions(options option.DebugOptions) {
|
func applyDebugOptions(options option.DebugOptions) {
|
||||||
|
applyDebugListenOption(options)
|
||||||
if options.GCPercent != nil {
|
if options.GCPercent != nil {
|
||||||
debug.SetGCPercent(*options.GCPercent)
|
debug.SetGCPercent(*options.GCPercent)
|
||||||
}
|
}
|
||||||
67
debug_http.go
Normal file
67
debug_http.go
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
package box
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/pprof"
|
||||||
|
"runtime"
|
||||||
|
"runtime/debug"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/common/badjson"
|
||||||
|
"github.com/sagernet/sing-box/common/json"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
|
||||||
|
"github.com/dustin/go-humanize"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
var debugHTTPServer *http.Server
|
||||||
|
|
||||||
|
func applyDebugListenOption(options option.DebugOptions) {
|
||||||
|
if debugHTTPServer != nil {
|
||||||
|
debugHTTPServer.Close()
|
||||||
|
debugHTTPServer = nil
|
||||||
|
}
|
||||||
|
if options.Listen == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r := chi.NewMux()
|
||||||
|
r.Route("/debug", func(r chi.Router) {
|
||||||
|
r.Get("/gc", func(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
writer.WriteHeader(http.StatusNoContent)
|
||||||
|
go debug.FreeOSMemory()
|
||||||
|
})
|
||||||
|
r.Get("/memory", func(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
var memStats runtime.MemStats
|
||||||
|
runtime.ReadMemStats(&memStats)
|
||||||
|
|
||||||
|
var memObject badjson.JSONObject
|
||||||
|
memObject.Put("heap", humanize.IBytes(memStats.HeapInuse))
|
||||||
|
memObject.Put("stack", humanize.IBytes(memStats.StackInuse))
|
||||||
|
memObject.Put("idle", humanize.IBytes(memStats.HeapIdle-memStats.HeapReleased))
|
||||||
|
memObject.Put("goroutines", runtime.NumGoroutine())
|
||||||
|
memObject.Put("rss", rusageMaxRSS())
|
||||||
|
|
||||||
|
encoder := json.NewEncoder(writer)
|
||||||
|
encoder.SetIndent("", " ")
|
||||||
|
encoder.Encode(memObject)
|
||||||
|
})
|
||||||
|
r.HandleFunc("/pprof", pprof.Index)
|
||||||
|
r.HandleFunc("/pprof/*", pprof.Index)
|
||||||
|
r.HandleFunc("/pprof/cmdline", pprof.Cmdline)
|
||||||
|
r.HandleFunc("/pprof/profile", pprof.Profile)
|
||||||
|
r.HandleFunc("/pprof/symbol", pprof.Symbol)
|
||||||
|
r.HandleFunc("/pprof/trace", pprof.Trace)
|
||||||
|
})
|
||||||
|
debugHTTPServer = &http.Server{
|
||||||
|
Addr: options.Listen,
|
||||||
|
Handler: r,
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
err := debugHTTPServer.ListenAndServe()
|
||||||
|
if err != nil && !E.IsClosed(err) {
|
||||||
|
log.Error(E.Cause(err, "serve debug HTTP server"))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
//go:build debug
|
package box
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"runtime"
|
"runtime"
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
//go:build debug && !linux
|
//go:build !linux
|
||||||
|
|
||||||
package main
|
package box
|
||||||
|
|
||||||
func rusageMaxRSS() float64 {
|
func rusageMaxRSS() float64 {
|
||||||
return -1
|
return -1
|
||||||
@@ -1,11 +1,240 @@
|
|||||||
#### 1.2.7
|
#### 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
|
* Fix bugs and update dependencies
|
||||||
|
|
||||||
|
#### 1.3.1-beta.3
|
||||||
|
|
||||||
|
* Introducing our [new iOS](/installation/clients/sfi) and [macOS](/installation/clients/sfm) client applications **1**
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
**1**:
|
||||||
|
|
||||||
|
The old testflight link and app are no longer valid.
|
||||||
|
|
||||||
|
#### 1.3.1-beta.2
|
||||||
|
|
||||||
|
* Fix bugs and update dependencies
|
||||||
|
|
||||||
|
#### 1.3.1-beta.1
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.3.0
|
||||||
|
|
||||||
|
* Fix bugs and update dependencies
|
||||||
|
|
||||||
|
Important changes since 1.2:
|
||||||
|
|
||||||
|
* Add [FakeIP](/configuration/dns/fakeip) support **1**
|
||||||
|
* Improve multiplex **2**
|
||||||
|
* Add [DNS reverse mapping](/configuration/dns#reverse_mapping) support
|
||||||
|
* Add `rewrite_ttl` DNS rule action
|
||||||
|
* Add `store_fakeip` Clash API option
|
||||||
|
* Add multi-peer support for [WireGuard](/configuration/outbound/wireguard#peers) outbound
|
||||||
|
* Add loopback detect
|
||||||
|
* Add Clash.Meta API compatibility for Clash API
|
||||||
|
* Download Yacd-meta by default if the specified Clash `external_ui` directory is empty
|
||||||
|
* Add path and headers option for HTTP outbound
|
||||||
|
* Perform URLTest recheck after network changes
|
||||||
|
* Fix `system` tun stack for ios
|
||||||
|
* Fix network monitor for android/ios
|
||||||
|
* Update VLESS and XUDP protocol
|
||||||
|
* Make splice work with traffic statistics systems like Clash API
|
||||||
|
* Significantly reduces memory usage of idle connections
|
||||||
|
* Improve DNS caching
|
||||||
|
* Add `independent_cache` [option](/configuration/dns#independent_cache) for DNS
|
||||||
|
* Reimplemented shadowsocks client
|
||||||
|
* Add multiplex support for VLESS outbound
|
||||||
|
* Automatically add Windows firewall rules in order for the system tun stack to work
|
||||||
|
* Fix TLS 1.2 support for shadow-tls client
|
||||||
|
* Add `cache_id` [option](/configuration/experimental#cache_id) for Clash cache file
|
||||||
|
* Fix `local` DNS transport for Android
|
||||||
|
|
||||||
|
*1*:
|
||||||
|
|
||||||
|
See [FAQ](/faq/fakeip) for more information.
|
||||||
|
|
||||||
|
*2*:
|
||||||
|
|
||||||
|
Added new `h2mux` multiplex protocol and `padding` multiplex option, see [Multiplex](/configuration/shared/multiplex).
|
||||||
|
|
||||||
|
#### 1.3-rc2
|
||||||
|
|
||||||
|
* Fix `local` DNS transport for Android
|
||||||
|
* Fix bugs and update dependencies
|
||||||
|
|
||||||
|
#### 1.3-rc1
|
||||||
|
|
||||||
|
* Fix bugs and update dependencies
|
||||||
|
|
||||||
|
#### 1.3-beta14
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.3-beta13
|
||||||
|
|
||||||
|
* Fix resolving fakeip domains **1**
|
||||||
|
* Deprecate L3 routing
|
||||||
|
* Fix bugs and update dependencies
|
||||||
|
|
||||||
|
**1**:
|
||||||
|
|
||||||
|
If the destination address of the connection is obtained from fakeip, dns rules with server type fakeip will be skipped.
|
||||||
|
|
||||||
|
#### 1.3-beta12
|
||||||
|
|
||||||
|
* Automatically add Windows firewall rules in order for the system tun stack to work
|
||||||
|
* Fix TLS 1.2 support for shadow-tls client
|
||||||
|
* Add `cache_id` [option](/configuration/experimental#cache_id) for Clash cache file
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.3-beta11
|
||||||
|
|
||||||
|
* Fix bugs and update dependencies
|
||||||
|
|
||||||
|
#### 1.3-beta10
|
||||||
|
|
||||||
|
* Improve direct copy **1**
|
||||||
|
* Improve DNS caching
|
||||||
|
* Add `independent_cache` [option](/configuration/dns#independent_cache) for DNS
|
||||||
|
* Reimplemented shadowsocks client **2**
|
||||||
|
* Add multiplex support for VLESS outbound
|
||||||
|
* Set TCP keepalive for WireGuard gVisor TCP connections
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
**1**:
|
||||||
|
|
||||||
|
* Make splice work with traffic statistics systems like Clash API
|
||||||
|
* Significantly reduces memory usage of idle connections
|
||||||
|
|
||||||
|
**2**:
|
||||||
|
|
||||||
|
Improved performance and reduced memory usage.
|
||||||
|
|
||||||
|
#### 1.3-beta9
|
||||||
|
|
||||||
|
* Improve multiplex **1**
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
*1*:
|
||||||
|
|
||||||
|
Added new `h2mux` multiplex protocol and `padding` multiplex option, see [Multiplex](/configuration/shared/multiplex).
|
||||||
|
|
||||||
#### 1.2.6
|
#### 1.2.6
|
||||||
|
|
||||||
* Fix bugs and update dependencies
|
* Fix bugs and update dependencies
|
||||||
|
|
||||||
|
#### 1.3-beta8
|
||||||
|
|
||||||
|
* Fix `system` tun stack for ios
|
||||||
|
* Fix network monitor for android/ios
|
||||||
|
* Update VLESS and XUDP protocol **1**
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
*1:
|
||||||
|
|
||||||
|
This is an incompatible update for XUDP in VLESS if vision flow is enabled.
|
||||||
|
|
||||||
|
#### 1.3-beta7
|
||||||
|
|
||||||
|
* Add `path` and `headers` options for HTTP outbound
|
||||||
|
* Add multi-user support for Shadowsocks legacy AEAD inbound
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.2.4
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.3-beta6
|
||||||
|
|
||||||
|
* Fix WireGuard reconnect
|
||||||
|
* Perform URLTest recheck after network changes
|
||||||
|
* Fix bugs and update dependencies
|
||||||
|
|
||||||
|
#### 1.3-beta5
|
||||||
|
|
||||||
|
* Add Clash.Meta API compatibility for Clash API
|
||||||
|
* Download Yacd-meta by default if the specified Clash `external_ui` directory is empty
|
||||||
|
* Add path and headers option for HTTP outbound
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.3-beta4
|
||||||
|
|
||||||
|
* Fix bugs
|
||||||
|
|
||||||
|
#### 1.3-beta2
|
||||||
|
|
||||||
|
* Download clash-dashboard if the specified Clash `external_ui` directory is empty
|
||||||
|
* Fix bugs and update dependencies
|
||||||
|
|
||||||
|
#### 1.3-beta1
|
||||||
|
|
||||||
|
* Add [DNS reverse mapping](/configuration/dns#reverse_mapping) support
|
||||||
|
* Add [L3 routing](/configuration/route/ip-rule) support **1**
|
||||||
|
* Add `rewrite_ttl` DNS rule action
|
||||||
|
* Add [FakeIP](/configuration/dns/fakeip) support **2**
|
||||||
|
* Add `store_fakeip` Clash API option
|
||||||
|
* Add multi-peer support for [WireGuard](/configuration/outbound/wireguard#peers) outbound
|
||||||
|
* Add loopback detect
|
||||||
|
|
||||||
|
*1*:
|
||||||
|
|
||||||
|
It can currently be used to [route connections directly to WireGuard](/examples/wireguard-direct) or block connections
|
||||||
|
at the IP layer.
|
||||||
|
|
||||||
|
*2*:
|
||||||
|
|
||||||
|
See [FAQ](/faq/fakeip) for more information.
|
||||||
|
|
||||||
#### 1.2.3
|
#### 1.2.3
|
||||||
|
|
||||||
* Introducing our [new Android client application](/installation/clients/sfa)
|
* Introducing our [new Android client application](/installation/clients/sfa)
|
||||||
|
|||||||
25
docs/configuration/dns/fakeip.md
Normal file
25
docs/configuration/dns/fakeip.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# FakeIP
|
||||||
|
|
||||||
|
### Structure
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"inet4_range": "198.18.0.0/15",
|
||||||
|
"inet6_range": "fc00::/18"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fields
|
||||||
|
|
||||||
|
#### enabled
|
||||||
|
|
||||||
|
Enable FakeIP service.
|
||||||
|
|
||||||
|
#### inet4_range
|
||||||
|
|
||||||
|
IPv4 address range for FakeIP.
|
||||||
|
|
||||||
|
#### inet6_address
|
||||||
|
|
||||||
|
IPv6 address range for FakeIP.
|
||||||
25
docs/configuration/dns/fakeip.zh.md
Normal file
25
docs/configuration/dns/fakeip.zh.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# FakeIP
|
||||||
|
|
||||||
|
### 结构
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"inet4_range": "198.18.0.0/15",
|
||||||
|
"inet6_range": "fc00::/18"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 字段
|
||||||
|
|
||||||
|
#### enabled
|
||||||
|
|
||||||
|
启用 FakeIP 服务。
|
||||||
|
|
||||||
|
#### inet4_range
|
||||||
|
|
||||||
|
用于 FakeIP 的 IPv4 地址范围。
|
||||||
|
|
||||||
|
#### inet6_range
|
||||||
|
|
||||||
|
用于 FakeIP 的 IPv6 地址范围。
|
||||||
@@ -10,7 +10,10 @@
|
|||||||
"final": "",
|
"final": "",
|
||||||
"strategy": "",
|
"strategy": "",
|
||||||
"disable_cache": false,
|
"disable_cache": false,
|
||||||
"disable_expire": false
|
"disable_expire": false,
|
||||||
|
"independent_cache": false,
|
||||||
|
"reverse_mapping": false,
|
||||||
|
"fakeip": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,6 +25,7 @@
|
|||||||
|----------|--------------------------------|
|
|----------|--------------------------------|
|
||||||
| `server` | List of [DNS Server](./server) |
|
| `server` | List of [DNS Server](./server) |
|
||||||
| `rules` | List of [DNS Rule](./rule) |
|
| `rules` | List of [DNS Rule](./rule) |
|
||||||
|
| `fakeip` | [FakeIP](./fakeip) |
|
||||||
|
|
||||||
#### final
|
#### final
|
||||||
|
|
||||||
@@ -43,4 +47,19 @@ Disable dns cache.
|
|||||||
|
|
||||||
#### disable_expire
|
#### disable_expire
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
Stores a reverse mapping of IP addresses after responding to a DNS query in order to provide domain names when routing.
|
||||||
|
|
||||||
|
Since this process relies on the act of resolving domain names by an application before making a request, it can be
|
||||||
|
problematic in environments such as macOS, where DNS is proxied and cached by the system.
|
||||||
|
|
||||||
|
#### fakeip
|
||||||
|
|
||||||
|
[FakeIP](./fakeip) settings.
|
||||||
|
|||||||
@@ -10,7 +10,10 @@
|
|||||||
"final": "",
|
"final": "",
|
||||||
"strategy": "",
|
"strategy": "",
|
||||||
"disable_cache": false,
|
"disable_cache": false,
|
||||||
"disable_expire": false
|
"disable_expire": false,
|
||||||
|
"independent_cache": false,
|
||||||
|
"reverse_mapping": false,
|
||||||
|
"fakeip": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,4 +46,18 @@
|
|||||||
|
|
||||||
#### disable_expire
|
#### disable_expire
|
||||||
|
|
||||||
禁用 DNS 缓存过期。
|
禁用 DNS 缓存过期。
|
||||||
|
|
||||||
|
#### independent_cache
|
||||||
|
|
||||||
|
使每个 DNS 服务器的缓存独立,以满足特殊目的。如果启用,将轻微降低性能。
|
||||||
|
|
||||||
|
#### reverse_mapping
|
||||||
|
|
||||||
|
在响应 DNS 查询后存储 IP 地址的反向映射以为路由目的提供域名。
|
||||||
|
|
||||||
|
由于此过程依赖于应用程序在发出请求之前解析域名的行为,因此在 macOS 等 DNS 由系统代理和缓存的环境中可能会出现问题。
|
||||||
|
|
||||||
|
#### fakeip
|
||||||
|
|
||||||
|
[FakeIP](./fakeip) 设置。
|
||||||
|
|||||||
@@ -84,14 +84,16 @@
|
|||||||
"direct"
|
"direct"
|
||||||
],
|
],
|
||||||
"server": "local",
|
"server": "local",
|
||||||
"disable_cache": false
|
"disable_cache": false,
|
||||||
|
"rewrite_ttl": 100
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "logical",
|
"type": "logical",
|
||||||
"mode": "and",
|
"mode": "and",
|
||||||
"rules": [],
|
"rules": [],
|
||||||
"server": "local",
|
"server": "local",
|
||||||
"disable_cache": false
|
"disable_cache": false,
|
||||||
|
"rewrite_ttl": 100
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -244,6 +246,10 @@ Tag of the target dns server.
|
|||||||
|
|
||||||
Disable cache and save cache in this query.
|
Disable cache and save cache in this query.
|
||||||
|
|
||||||
|
#### rewrite_ttl
|
||||||
|
|
||||||
|
Rewrite TTL in DNS responses.
|
||||||
|
|
||||||
### Logical Fields
|
### Logical Fields
|
||||||
|
|
||||||
#### type
|
#### type
|
||||||
|
|||||||
@@ -243,6 +243,10 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
|
|||||||
|
|
||||||
在此查询中禁用缓存。
|
在此查询中禁用缓存。
|
||||||
|
|
||||||
|
#### rewrite_ttl
|
||||||
|
|
||||||
|
重写 DNS 回应中的 TTL。
|
||||||
|
|
||||||
### 逻辑字段
|
### 逻辑字段
|
||||||
|
|
||||||
#### type
|
#### type
|
||||||
|
|||||||
@@ -30,17 +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` |
|
||||||
|
|
||||||
!!! warning ""
|
!!! warning ""
|
||||||
|
|
||||||
@@ -93,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,17 +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` |
|
||||||
|
|
||||||
!!! warning ""
|
!!! warning ""
|
||||||
|
|
||||||
|
|||||||
@@ -7,11 +7,14 @@
|
|||||||
"experimental": {
|
"experimental": {
|
||||||
"clash_api": {
|
"clash_api": {
|
||||||
"external_controller": "127.0.0.1:9090",
|
"external_controller": "127.0.0.1:9090",
|
||||||
"external_ui": "folder",
|
"external_ui": "",
|
||||||
|
"external_ui_download_url": "",
|
||||||
|
"external_ui_download_detour": "",
|
||||||
"secret": "",
|
"secret": "",
|
||||||
"default_mode": "rule",
|
"default_mode": "",
|
||||||
"store_selected": false,
|
"store_selected": false,
|
||||||
"cache_file": "cache.db"
|
"cache_file": "",
|
||||||
|
"cache_id": ""
|
||||||
},
|
},
|
||||||
"v2ray_api": {
|
"v2ray_api": {
|
||||||
"listen": "127.0.0.1:8080",
|
"listen": "127.0.0.1:8080",
|
||||||
@@ -53,6 +56,18 @@ A relative path to the configuration directory or an absolute path to a
|
|||||||
directory in which you put some static web resource. sing-box will then
|
directory in which you put some static web resource. sing-box will then
|
||||||
serve it at `http://{{external-controller}}/ui`.
|
serve it at `http://{{external-controller}}/ui`.
|
||||||
|
|
||||||
|
#### external_ui_download_url
|
||||||
|
|
||||||
|
ZIP download URL for the external UI, will be used if the specified `external_ui` directory is empty.
|
||||||
|
|
||||||
|
`https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip` will be used if empty.
|
||||||
|
|
||||||
|
#### external_ui_download_detour
|
||||||
|
|
||||||
|
The tag of the outbound to download the external UI.
|
||||||
|
|
||||||
|
Default outbound will be used if empty.
|
||||||
|
|
||||||
#### secret
|
#### secret
|
||||||
|
|
||||||
Secret for the RESTful API (optional)
|
Secret for the RESTful API (optional)
|
||||||
@@ -77,6 +92,12 @@ 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,11 +7,14 @@
|
|||||||
"experimental": {
|
"experimental": {
|
||||||
"clash_api": {
|
"clash_api": {
|
||||||
"external_controller": "127.0.0.1:9090",
|
"external_controller": "127.0.0.1:9090",
|
||||||
"external_ui": "folder",
|
"external_ui": "",
|
||||||
|
"external_ui_download_url": "",
|
||||||
|
"external_ui_download_detour": "",
|
||||||
"secret": "",
|
"secret": "",
|
||||||
"default_mode": "rule",
|
"default_mode": "",
|
||||||
"store_selected": false,
|
"store_selected": false,
|
||||||
"cache_file": "cache.db"
|
"cache_file": "",
|
||||||
|
"cache_id": ""
|
||||||
},
|
},
|
||||||
"v2ray_api": {
|
"v2ray_api": {
|
||||||
"listen": "127.0.0.1:8080",
|
"listen": "127.0.0.1:8080",
|
||||||
@@ -51,6 +54,18 @@ RESTful web API 监听地址。如果为空,则禁用 Clash API。
|
|||||||
|
|
||||||
到静态网页资源目录的相对路径或绝对路径。sing-box 会在 `http://{{external-controller}}/ui` 下提供它。
|
到静态网页资源目录的相对路径或绝对路径。sing-box 会在 `http://{{external-controller}}/ui` 下提供它。
|
||||||
|
|
||||||
|
#### external_ui_download_url
|
||||||
|
|
||||||
|
静态网页资源的 ZIP 下载 URL,如果指定的 `external_ui` 目录为空,将使用。
|
||||||
|
|
||||||
|
默认使用 `https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip`。
|
||||||
|
|
||||||
|
#### external_ui_download_detour
|
||||||
|
|
||||||
|
用于下载静态网页资源的出站的标签。
|
||||||
|
|
||||||
|
如果为空,将使用默认出站。
|
||||||
|
|
||||||
#### secret
|
#### secret
|
||||||
|
|
||||||
RESTful API 的密钥(可选)
|
RESTful API 的密钥(可选)
|
||||||
@@ -75,6 +90,12 @@ Clash 中的默认模式,默认使用 `rule`。
|
|||||||
|
|
||||||
缓存文件路径,默认使用`cache.db`。
|
缓存文件路径,默认使用`cache.db`。
|
||||||
|
|
||||||
|
#### cache_id
|
||||||
|
|
||||||
|
缓存 ID。
|
||||||
|
|
||||||
|
如果不为空,`store_selected` 将会使用以此为键的独立存储。
|
||||||
|
|
||||||
### V2Ray API 字段
|
### V2Ray API 字段
|
||||||
|
|
||||||
!!! error ""
|
!!! error ""
|
||||||
|
|||||||
82
docs/configuration/inbound/tuic.md
Normal file
82
docs/configuration/inbound/tuic.md
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
### 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).
|
||||||
82
docs/configuration/inbound/tuic.zh.md
Normal file
82
docs/configuration/inbound/tuic.zh.md
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
### 结构
|
||||||
|
|
||||||
|
```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,11 +142,12 @@ UDP NAT expiration time in seconds, default is 300 (5 minutes).
|
|||||||
|
|
||||||
TCP/IP stack.
|
TCP/IP stack.
|
||||||
|
|
||||||
| Stack | Description | Status |
|
| Stack | Description | Status |
|
||||||
|------------------|----------------------------------------------------------------------------------|-------------------|
|
|--------|----------------------------------------------------------------------------------|-------------------|
|
||||||
| system (default) | Sometimes better performance | recommended |
|
| system | 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 |
|
||||||
| LWIP | Based on [eycorsican/go-tun2socks](https://github.com/eycorsican/go-tun2socks) | upstream archived |
|
| mixed | Mixed `system` TCP stack and `gVisor` UDP stack | recommended |
|
||||||
|
| LWIP | Based on [eycorsican/go-tun2socks](https://github.com/eycorsican/go-tun2socks) | upstream archived |
|
||||||
|
|
||||||
!!! warning ""
|
!!! warning ""
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
"server_port": 1080,
|
"server_port": 1080,
|
||||||
"username": "sekai",
|
"username": "sekai",
|
||||||
"password": "admin",
|
"password": "admin",
|
||||||
|
"path": "",
|
||||||
|
"headers": {},
|
||||||
"tls": {},
|
"tls": {},
|
||||||
|
|
||||||
... // Dial Fields
|
... // Dial Fields
|
||||||
@@ -39,6 +41,14 @@ Basic authorization username.
|
|||||||
|
|
||||||
Basic authorization password.
|
Basic authorization password.
|
||||||
|
|
||||||
|
#### path
|
||||||
|
|
||||||
|
Path of HTTP request.
|
||||||
|
|
||||||
|
#### headers
|
||||||
|
|
||||||
|
Extra headers of HTTP request.
|
||||||
|
|
||||||
#### tls
|
#### tls
|
||||||
|
|
||||||
TLS configuration, see [TLS](/configuration/shared/tls/#outbound).
|
TLS configuration, see [TLS](/configuration/shared/tls/#outbound).
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
"server_port": 1080,
|
"server_port": 1080,
|
||||||
"username": "sekai",
|
"username": "sekai",
|
||||||
"password": "admin",
|
"password": "admin",
|
||||||
|
"path": "",
|
||||||
|
"headers": {},
|
||||||
"tls": {},
|
"tls": {},
|
||||||
|
|
||||||
... // 拨号字段
|
... // 拨号字段
|
||||||
@@ -39,6 +41,14 @@ Basic 认证用户名。
|
|||||||
|
|
||||||
Basic 认证密码。
|
Basic 认证密码。
|
||||||
|
|
||||||
|
#### path
|
||||||
|
|
||||||
|
HTTP 请求路径。
|
||||||
|
|
||||||
|
#### headers
|
||||||
|
|
||||||
|
HTTP 请求的额外标头。
|
||||||
|
|
||||||
#### tls
|
#### tls
|
||||||
|
|
||||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
|
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
|
||||||
|
|||||||
@@ -97,12 +97,6 @@ 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
|
||||||
@@ -111,6 +105,12 @@ 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,10 +97,6 @@ base64 编码的认证密码。
|
|||||||
|
|
||||||
强制为 Linux 和 Windows 以外的系统启用(根据上游)。
|
强制为 Linux 和 Windows 以外的系统启用(根据上游)。
|
||||||
|
|
||||||
==必填==
|
|
||||||
|
|
||||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
|
|
||||||
|
|
||||||
#### network
|
#### network
|
||||||
|
|
||||||
启用的网络协议。
|
启用的网络协议。
|
||||||
@@ -109,6 +105,13 @@ TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
|
|||||||
|
|
||||||
默认所有。
|
默认所有。
|
||||||
|
|
||||||
|
#### tls
|
||||||
|
|
||||||
|
==必填==
|
||||||
|
|
||||||
|
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
|
||||||
|
|
||||||
|
|
||||||
### 拨号字段
|
### 拨号字段
|
||||||
|
|
||||||
参阅 [拨号字段](/zh/configuration/shared/dial/)。
|
参阅 [拨号字段](/zh/configuration/shared/dial/)。
|
||||||
|
|||||||
@@ -37,4 +37,10 @@
|
|||||||
|
|
||||||
#### tag
|
#### tag
|
||||||
|
|
||||||
The tag of the outbound.
|
The tag of the outbound.
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
#### Outbounds that support IP connection
|
||||||
|
|
||||||
|
* `WireGuard`
|
||||||
|
|||||||
@@ -36,4 +36,10 @@
|
|||||||
|
|
||||||
#### tag
|
#### tag
|
||||||
|
|
||||||
出站的标签。
|
出站的标签。
|
||||||
|
|
||||||
|
### 特性
|
||||||
|
|
||||||
|
#### 支持 IP 连接的出站
|
||||||
|
|
||||||
|
* `WireGuard`
|
||||||
|
|||||||
86
docs/configuration/outbound/tuic.md
Normal file
86
docs/configuration/outbound/tuic.md
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
### 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.
|
||||||
98
docs/configuration/outbound/tuic.zh.md
Normal file
98
docs/configuration/outbound/tuic.zh.md
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
### 结构
|
||||||
|
|
||||||
|
```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/)。
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
"proxy-b",
|
"proxy-b",
|
||||||
"proxy-c"
|
"proxy-c"
|
||||||
],
|
],
|
||||||
"url": "http://www.gstatic.com/generate_204",
|
"url": "https://www.gstatic.com/generate_204",
|
||||||
"interval": "1m",
|
"interval": "1m",
|
||||||
"tolerance": 50
|
"tolerance": 50
|
||||||
}
|
}
|
||||||
@@ -26,7 +26,7 @@ List of outbound tags to test.
|
|||||||
|
|
||||||
#### url
|
#### url
|
||||||
|
|
||||||
The URL to test. `http://www.gstatic.com/generate_204` will be used if empty.
|
The URL to test. `https://www.gstatic.com/generate_204` will be used if empty.
|
||||||
|
|
||||||
#### interval
|
#### interval
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
"proxy-b",
|
"proxy-b",
|
||||||
"proxy-c"
|
"proxy-c"
|
||||||
],
|
],
|
||||||
"url": "http://www.gstatic.com/generate_204",
|
"url": "https://www.gstatic.com/generate_204",
|
||||||
"interval": "1m",
|
"interval": "1m",
|
||||||
"tolerance": 50
|
"tolerance": 50
|
||||||
}
|
}
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
|
|
||||||
#### url
|
#### url
|
||||||
|
|
||||||
用于测试的链接。默认使用 `http://www.gstatic.com/generate_204`。
|
用于测试的链接。默认使用 `https://www.gstatic.com/generate_204`。
|
||||||
|
|
||||||
#### interval
|
#### interval
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,18 @@
|
|||||||
"10.0.0.2/32"
|
"10.0.0.2/32"
|
||||||
],
|
],
|
||||||
"private_key": "YNXtAzepDqRv9H52osJVDQnznT5AM11eCK3ESpwSt04=",
|
"private_key": "YNXtAzepDqRv9H52osJVDQnznT5AM11eCK3ESpwSt04=",
|
||||||
|
"peers": [
|
||||||
|
{
|
||||||
|
"server": "127.0.0.1",
|
||||||
|
"server_port": 1080,
|
||||||
|
"public_key": "Z1XXLsKYkYxuiYjJIkRvtIKFepCYHTgON+GwPq7SOV4=",
|
||||||
|
"pre_shared_key": "31aIhAPwktDGpH4JDhA8GNvjFXEf/a6+UaQRyOAiyfM=",
|
||||||
|
"allowed_ips": [
|
||||||
|
"0.0.0.0/0"
|
||||||
|
],
|
||||||
|
"reserved": [0, 0, 0]
|
||||||
|
}
|
||||||
|
],
|
||||||
"peer_public_key": "Z1XXLsKYkYxuiYjJIkRvtIKFepCYHTgON+GwPq7SOV4=",
|
"peer_public_key": "Z1XXLsKYkYxuiYjJIkRvtIKFepCYHTgON+GwPq7SOV4=",
|
||||||
"pre_shared_key": "31aIhAPwktDGpH4JDhA8GNvjFXEf/a6+UaQRyOAiyfM=",
|
"pre_shared_key": "31aIhAPwktDGpH4JDhA8GNvjFXEf/a6+UaQRyOAiyfM=",
|
||||||
"reserved": [0, 0, 0],
|
"reserved": [0, 0, 0],
|
||||||
@@ -36,13 +48,13 @@
|
|||||||
|
|
||||||
#### server
|
#### server
|
||||||
|
|
||||||
==Required==
|
==Required if multi-peer disabled==
|
||||||
|
|
||||||
The server address.
|
The server address.
|
||||||
|
|
||||||
#### server_port
|
#### server_port
|
||||||
|
|
||||||
==Required==
|
==Required if multi-peer disabled==
|
||||||
|
|
||||||
The server port.
|
The server port.
|
||||||
|
|
||||||
@@ -75,9 +87,25 @@ wg genkey
|
|||||||
echo "private key" || wg pubkey
|
echo "private key" || wg pubkey
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### peers
|
||||||
|
|
||||||
|
Multi-peer support.
|
||||||
|
|
||||||
|
If enabled, `server, server_port, peer_public_key, pre_shared_key` will be ignored.
|
||||||
|
|
||||||
|
#### peers.allowed_ips
|
||||||
|
|
||||||
|
WireGuard allowed IPs.
|
||||||
|
|
||||||
|
#### peers.reserved
|
||||||
|
|
||||||
|
WireGuard reserved field bytes.
|
||||||
|
|
||||||
|
`$outbound.reserved` will be used if empty.
|
||||||
|
|
||||||
#### peer_public_key
|
#### peer_public_key
|
||||||
|
|
||||||
==Required==
|
==Required if multi-peer disabled==
|
||||||
|
|
||||||
WireGuard peer public key.
|
WireGuard peer public key.
|
||||||
|
|
||||||
|
|||||||
@@ -19,11 +19,11 @@
|
|||||||
|
|
||||||
### Fields
|
### Fields
|
||||||
|
|
||||||
| Key | Format |
|
| Key | Format |
|
||||||
|-----------|------------------------------|
|
|------------|------------------------------------|
|
||||||
| `geoip` | [GeoIP](./geoip) |
|
| `geoip` | [GeoIP](./geoip) |
|
||||||
| `geosite` | [Geosite](./geosite) |
|
| `geosite` | [Geosite](./geosite) |
|
||||||
| `rules` | List of [Route Rule](./rule) |
|
| `rules` | List of [Route Rule](./rule) |
|
||||||
|
|
||||||
#### final
|
#### final
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
"route": {
|
"route": {
|
||||||
"geoip": {},
|
"geoip": {},
|
||||||
"geosite": {},
|
"geosite": {},
|
||||||
|
"ip_rules": [],
|
||||||
"rules": [],
|
"rules": [],
|
||||||
"final": "",
|
"final": "",
|
||||||
"auto_detect_interface": false,
|
"auto_detect_interface": false,
|
||||||
@@ -19,11 +20,12 @@
|
|||||||
|
|
||||||
### 字段
|
### 字段
|
||||||
|
|
||||||
| 键 | 格式 |
|
| 键 | 格式 |
|
||||||
|-----------|----------------------|
|
|------------|-------------------------|
|
||||||
| `geoip` | [GeoIP](./geoip) |
|
| `geoip` | [GeoIP](./geoip) |
|
||||||
| `geosite` | [GeoSite](./geosite) |
|
| `geosite` | [GeoSite](./geosite) |
|
||||||
| `rules` | 一组 [路由规则](./rule) |
|
| `ip_rules` | 一组 [IP 路由规则](./ip-rule) |
|
||||||
|
| `rules` | 一组 [路由规则](./rule) |
|
||||||
|
|
||||||
#### final
|
#### final
|
||||||
|
|
||||||
@@ -65,4 +67,4 @@
|
|||||||
|
|
||||||
默认为出站连接设置路由标记。
|
默认为出站连接设置路由标记。
|
||||||
|
|
||||||
如果设置了 `outbound.routing_mark` 设置,则不生效。
|
如果设置了 `outbound.routing_mark` 设置,则不生效。
|
||||||
205
docs/configuration/route/ip-rule.md
Normal file
205
docs/configuration/route/ip-rule.md
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
### Structure
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"route": {
|
||||||
|
"ip_rules": [
|
||||||
|
{
|
||||||
|
"inbound": [
|
||||||
|
"mixed-in"
|
||||||
|
],
|
||||||
|
"ip_version": 6,
|
||||||
|
"network": [
|
||||||
|
"tcp"
|
||||||
|
],
|
||||||
|
"domain": [
|
||||||
|
"test.com"
|
||||||
|
],
|
||||||
|
"domain_suffix": [
|
||||||
|
".cn"
|
||||||
|
],
|
||||||
|
"domain_keyword": [
|
||||||
|
"test"
|
||||||
|
],
|
||||||
|
"domain_regex": [
|
||||||
|
"^stun\\..+"
|
||||||
|
],
|
||||||
|
"geosite": [
|
||||||
|
"cn"
|
||||||
|
],
|
||||||
|
"source_geoip": [
|
||||||
|
"private"
|
||||||
|
],
|
||||||
|
"geoip": [
|
||||||
|
"cn"
|
||||||
|
],
|
||||||
|
"source_ip_cidr": [
|
||||||
|
"10.0.0.0/24",
|
||||||
|
"192.168.0.1"
|
||||||
|
],
|
||||||
|
"ip_cidr": [
|
||||||
|
"10.0.0.0/24",
|
||||||
|
"192.168.0.1"
|
||||||
|
],
|
||||||
|
"source_port": [
|
||||||
|
12345
|
||||||
|
],
|
||||||
|
"source_port_range": [
|
||||||
|
"1000:2000",
|
||||||
|
":3000",
|
||||||
|
"4000:"
|
||||||
|
],
|
||||||
|
"port": [
|
||||||
|
80,
|
||||||
|
443
|
||||||
|
],
|
||||||
|
"port_range": [
|
||||||
|
"1000:2000",
|
||||||
|
":3000",
|
||||||
|
"4000:"
|
||||||
|
],
|
||||||
|
"invert": false,
|
||||||
|
"action": "direct",
|
||||||
|
"outbound": "wireguard"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "logical",
|
||||||
|
"mode": "and",
|
||||||
|
"rules": [],
|
||||||
|
"invert": false,
|
||||||
|
"action": "direct",
|
||||||
|
"outbound": "wireguard"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! note ""
|
||||||
|
|
||||||
|
You can ignore the JSON Array [] tag when the content is only one item
|
||||||
|
|
||||||
|
### Default Fields
|
||||||
|
|
||||||
|
!!! note ""
|
||||||
|
|
||||||
|
The default rule uses the following matching logic:
|
||||||
|
(`domain` || `domain_suffix` || `domain_keyword` || `domain_regex` || `geosite` || `geoip` || `ip_cidr`) &&
|
||||||
|
(`port` || `port_range`) &&
|
||||||
|
(`source_geoip` || `source_ip_cidr`) &&
|
||||||
|
(`source_port` || `source_port_range`) &&
|
||||||
|
`other fields`
|
||||||
|
|
||||||
|
#### inbound
|
||||||
|
|
||||||
|
Tags of [Inbound](/configuration/inbound).
|
||||||
|
|
||||||
|
#### ip_version
|
||||||
|
|
||||||
|
4 or 6.
|
||||||
|
|
||||||
|
Not limited if empty.
|
||||||
|
|
||||||
|
#### network
|
||||||
|
|
||||||
|
Match network protocol.
|
||||||
|
|
||||||
|
Available values:
|
||||||
|
|
||||||
|
* `tcp`
|
||||||
|
* `udp`
|
||||||
|
* `icmpv4`
|
||||||
|
* `icmpv6`
|
||||||
|
|
||||||
|
#### domain
|
||||||
|
|
||||||
|
Match full domain.
|
||||||
|
|
||||||
|
#### domain_suffix
|
||||||
|
|
||||||
|
Match domain suffix.
|
||||||
|
|
||||||
|
#### domain_keyword
|
||||||
|
|
||||||
|
Match domain using keyword.
|
||||||
|
|
||||||
|
#### domain_regex
|
||||||
|
|
||||||
|
Match domain using regular expression.
|
||||||
|
|
||||||
|
#### geosite
|
||||||
|
|
||||||
|
Match geosite.
|
||||||
|
|
||||||
|
#### source_geoip
|
||||||
|
|
||||||
|
Match source geoip.
|
||||||
|
|
||||||
|
#### geoip
|
||||||
|
|
||||||
|
Match geoip.
|
||||||
|
|
||||||
|
#### source_ip_cidr
|
||||||
|
|
||||||
|
Match source ip cidr.
|
||||||
|
|
||||||
|
#### ip_cidr
|
||||||
|
|
||||||
|
Match ip cidr.
|
||||||
|
|
||||||
|
#### source_port
|
||||||
|
|
||||||
|
Match source port.
|
||||||
|
|
||||||
|
#### source_port_range
|
||||||
|
|
||||||
|
Match source port range.
|
||||||
|
|
||||||
|
#### port
|
||||||
|
|
||||||
|
Match port.
|
||||||
|
|
||||||
|
#### port_range
|
||||||
|
|
||||||
|
Match port range.
|
||||||
|
|
||||||
|
#### invert
|
||||||
|
|
||||||
|
Invert match result.
|
||||||
|
|
||||||
|
#### action
|
||||||
|
|
||||||
|
==Required==
|
||||||
|
|
||||||
|
| Action | Description |
|
||||||
|
|--------|--------------------------------------------------------------------|
|
||||||
|
| return | Stop IP routing and assemble the connection to the transport layer |
|
||||||
|
| block | Block the connection |
|
||||||
|
| direct | Directly forward the connection |
|
||||||
|
|
||||||
|
#### outbound
|
||||||
|
|
||||||
|
==Required if action is direct==
|
||||||
|
|
||||||
|
Tag of the target outbound.
|
||||||
|
|
||||||
|
Only outbound which supports IP connection can be used, see [Outbounds that support IP connection](/configuration/outbound/#outbounds-that-support-ip-connection).
|
||||||
|
|
||||||
|
### Logical Fields
|
||||||
|
|
||||||
|
#### type
|
||||||
|
|
||||||
|
`logical`
|
||||||
|
|
||||||
|
#### mode
|
||||||
|
|
||||||
|
==Required==
|
||||||
|
|
||||||
|
`and` or `or`
|
||||||
|
|
||||||
|
#### rules
|
||||||
|
|
||||||
|
==Required==
|
||||||
|
|
||||||
|
Included default rules.
|
||||||
204
docs/configuration/route/ip-rule.zh.md
Normal file
204
docs/configuration/route/ip-rule.zh.md
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
### 结构
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"route": {
|
||||||
|
"ip_rules": [
|
||||||
|
{
|
||||||
|
"inbound": [
|
||||||
|
"mixed-in"
|
||||||
|
],
|
||||||
|
"ip_version": 6,
|
||||||
|
"network": [
|
||||||
|
"tcp"
|
||||||
|
],
|
||||||
|
"domain": [
|
||||||
|
"test.com"
|
||||||
|
],
|
||||||
|
"domain_suffix": [
|
||||||
|
".cn"
|
||||||
|
],
|
||||||
|
"domain_keyword": [
|
||||||
|
"test"
|
||||||
|
],
|
||||||
|
"domain_regex": [
|
||||||
|
"^stun\\..+"
|
||||||
|
],
|
||||||
|
"geosite": [
|
||||||
|
"cn"
|
||||||
|
],
|
||||||
|
"source_geoip": [
|
||||||
|
"private"
|
||||||
|
],
|
||||||
|
"geoip": [
|
||||||
|
"cn"
|
||||||
|
],
|
||||||
|
"source_ip_cidr": [
|
||||||
|
"10.0.0.0/24",
|
||||||
|
"192.168.0.1"
|
||||||
|
],
|
||||||
|
"ip_cidr": [
|
||||||
|
"10.0.0.0/24",
|
||||||
|
"192.168.0.1"
|
||||||
|
],
|
||||||
|
"source_port": [
|
||||||
|
12345
|
||||||
|
],
|
||||||
|
"source_port_range": [
|
||||||
|
"1000:2000",
|
||||||
|
":3000",
|
||||||
|
"4000:"
|
||||||
|
],
|
||||||
|
"port": [
|
||||||
|
80,
|
||||||
|
443
|
||||||
|
],
|
||||||
|
"port_range": [
|
||||||
|
"1000:2000",
|
||||||
|
":3000",
|
||||||
|
"4000:"
|
||||||
|
],
|
||||||
|
"invert": false,
|
||||||
|
"action": "direct",
|
||||||
|
"outbound": "wireguard"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "logical",
|
||||||
|
"mode": "and",
|
||||||
|
"rules": [],
|
||||||
|
"invert": false,
|
||||||
|
"action": "direct",
|
||||||
|
"outbound": "wireguard"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! note ""
|
||||||
|
|
||||||
|
当内容只有一项时,可以忽略 JSON 数组 [] 标签。
|
||||||
|
|
||||||
|
### Default Fields
|
||||||
|
|
||||||
|
!!! note ""
|
||||||
|
|
||||||
|
默认规则使用以下匹配逻辑:
|
||||||
|
(`domain` || `domain_suffix` || `domain_keyword` || `domain_regex` || `geosite` || `geoip` || `ip_cidr`) &&
|
||||||
|
(`port` || `port_range`) &&
|
||||||
|
(`source_geoip` || `source_ip_cidr`) &&
|
||||||
|
(`source_port` || `source_port_range`) &&
|
||||||
|
`other fields`
|
||||||
|
|
||||||
|
#### inbound
|
||||||
|
|
||||||
|
[入站](/zh/configuration/inbound) 标签。
|
||||||
|
|
||||||
|
#### ip_version
|
||||||
|
|
||||||
|
4 或 6。
|
||||||
|
|
||||||
|
默认不限制。
|
||||||
|
|
||||||
|
#### network
|
||||||
|
|
||||||
|
匹配网络协议。
|
||||||
|
|
||||||
|
可用值:
|
||||||
|
|
||||||
|
* `tcp`
|
||||||
|
* `udp`
|
||||||
|
* `icmpv4`
|
||||||
|
* `icmpv6`
|
||||||
|
|
||||||
|
#### domain
|
||||||
|
|
||||||
|
匹配完整域名。
|
||||||
|
|
||||||
|
#### domain_suffix
|
||||||
|
|
||||||
|
匹配域名后缀。
|
||||||
|
|
||||||
|
#### domain_keyword
|
||||||
|
|
||||||
|
匹配域名关键字。
|
||||||
|
|
||||||
|
#### domain_regex
|
||||||
|
|
||||||
|
匹配域名正则表达式。
|
||||||
|
|
||||||
|
#### geosite
|
||||||
|
|
||||||
|
匹配 GeoSite。
|
||||||
|
|
||||||
|
#### source_geoip
|
||||||
|
|
||||||
|
匹配源 GeoIP。
|
||||||
|
|
||||||
|
#### geoip
|
||||||
|
|
||||||
|
匹配 GeoIP。
|
||||||
|
|
||||||
|
#### source_ip_cidr
|
||||||
|
|
||||||
|
匹配源 IP CIDR。
|
||||||
|
|
||||||
|
#### ip_cidr
|
||||||
|
|
||||||
|
匹配 IP CIDR。
|
||||||
|
|
||||||
|
#### source_port
|
||||||
|
|
||||||
|
匹配源端口。
|
||||||
|
|
||||||
|
#### source_port_range
|
||||||
|
|
||||||
|
匹配源端口范围。
|
||||||
|
|
||||||
|
#### port
|
||||||
|
|
||||||
|
匹配端口。
|
||||||
|
|
||||||
|
#### port_range
|
||||||
|
|
||||||
|
匹配端口范围。
|
||||||
|
|
||||||
|
#### invert
|
||||||
|
|
||||||
|
反选匹配结果。
|
||||||
|
|
||||||
|
#### action
|
||||||
|
|
||||||
|
==必填==
|
||||||
|
|
||||||
|
| Action | 描述 |
|
||||||
|
|--------|---------------------|
|
||||||
|
| return | 停止 IP 路由并将该连接组装到传输层 |
|
||||||
|
| block | 屏蔽该连接 |
|
||||||
|
| direct | 直接转发该连接 |
|
||||||
|
|
||||||
|
|
||||||
|
#### outbound
|
||||||
|
|
||||||
|
==action 为 direct 则必填==
|
||||||
|
|
||||||
|
目标出站的标签。
|
||||||
|
|
||||||
|
### 逻辑字段
|
||||||
|
|
||||||
|
#### type
|
||||||
|
|
||||||
|
`logical`
|
||||||
|
|
||||||
|
#### mode
|
||||||
|
|
||||||
|
==必填==
|
||||||
|
|
||||||
|
`and` 或 `or`
|
||||||
|
|
||||||
|
#### rules
|
||||||
|
|
||||||
|
==必填==
|
||||||
|
|
||||||
|
包括的默认规则。
|
||||||
@@ -9,7 +9,9 @@
|
|||||||
"mixed-in"
|
"mixed-in"
|
||||||
],
|
],
|
||||||
"ip_version": 6,
|
"ip_version": 6,
|
||||||
"network": "tcp",
|
"network": [
|
||||||
|
"tcp"
|
||||||
|
],
|
||||||
"auth_user": [
|
"auth_user": [
|
||||||
"usera",
|
"usera",
|
||||||
"userb"
|
"userb"
|
||||||
@@ -244,18 +246,12 @@ Tag of the target outbound.
|
|||||||
|
|
||||||
#### mode
|
#### mode
|
||||||
|
|
||||||
|
==Required==
|
||||||
|
|
||||||
`and` or `or`
|
`and` or `or`
|
||||||
|
|
||||||
#### rules
|
#### rules
|
||||||
|
|
||||||
Included default rules.
|
|
||||||
|
|
||||||
#### invert
|
|
||||||
|
|
||||||
Invert match result.
|
|
||||||
|
|
||||||
#### outbound
|
|
||||||
|
|
||||||
==Required==
|
==Required==
|
||||||
|
|
||||||
Tag of the target outbound.
|
Included default rules.
|
||||||
|
|||||||
@@ -9,7 +9,9 @@
|
|||||||
"mixed-in"
|
"mixed-in"
|
||||||
],
|
],
|
||||||
"ip_version": 6,
|
"ip_version": 6,
|
||||||
"network": "tcp",
|
"network": [
|
||||||
|
"tcp"
|
||||||
|
],
|
||||||
"auth_user": [
|
"auth_user": [
|
||||||
"usera",
|
"usera",
|
||||||
"userb"
|
"userb"
|
||||||
@@ -242,18 +244,12 @@
|
|||||||
|
|
||||||
#### mode
|
#### mode
|
||||||
|
|
||||||
|
==必填==
|
||||||
|
|
||||||
`and` 或 `or`
|
`and` 或 `or`
|
||||||
|
|
||||||
#### rules
|
#### rules
|
||||||
|
|
||||||
包括的默认规则。
|
|
||||||
|
|
||||||
#### invert
|
|
||||||
|
|
||||||
反选匹配结果。
|
|
||||||
|
|
||||||
#### outbound
|
|
||||||
|
|
||||||
==必填==
|
==必填==
|
||||||
|
|
||||||
目标出站的标签。
|
包括的默认规则。
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
"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"
|
||||||
@@ -18,9 +19,9 @@
|
|||||||
|
|
||||||
### Fields
|
### Fields
|
||||||
|
|
||||||
| Field | Available Context |
|
| Field | Available Context |
|
||||||
|----------------------------------------------------------------------------------------------------------------------|-------------------|
|
|------------------------------------------------------------------------------------------------------------------------------------------|-------------------|
|
||||||
| `bind_interface` /`*bind_address` /`routing_mark` /`reuse_addr` / `tcp_fast_open`/ `udp_fragment` /`connect_timeout` | `detour` not set |
|
| `bind_interface` /`*bind_address` /`routing_mark` /`reuse_addr` / `tcp_fast_open` / `tcp_multi_path` / `udp_fragment` /`connect_timeout` | `detour` not set |
|
||||||
|
|
||||||
#### detour
|
#### detour
|
||||||
|
|
||||||
@@ -54,6 +55,14 @@ 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,6 +10,7 @@
|
|||||||
"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"
|
||||||
@@ -18,9 +19,9 @@
|
|||||||
|
|
||||||
### 字段
|
### 字段
|
||||||
|
|
||||||
| 字段 | 可用上下文 |
|
| 字段 | 可用上下文 |
|
||||||
|----------------------------------------------------------------------------------------------------------------------|--------------|
|
|------------------------------------------------------------------------------------------------------------------------------------------|--------------|
|
||||||
| `bind_interface` /`*bind_address` /`routing_mark` /`reuse_addr` / `tcp_fast_open`/ `udp_fragment` /`connect_timeout` | `detour` 未设置 |
|
| `bind_interface` /`*bind_address` /`routing_mark` /`reuse_addr` / `tcp_fast_open` / `tcp_mutli_path` / `udp_fragment` /`connect_timeout` | `detour` 未设置 |
|
||||||
|
|
||||||
|
|
||||||
#### detour
|
#### detour
|
||||||
@@ -57,6 +58,14 @@
|
|||||||
|
|
||||||
启用 TCP Fast Open。
|
启用 TCP Fast Open。
|
||||||
|
|
||||||
|
#### tcp_multi_path
|
||||||
|
|
||||||
|
!!! warning ""
|
||||||
|
|
||||||
|
需要 Go 1.21。
|
||||||
|
|
||||||
|
启用 TCP Multi Path。
|
||||||
|
|
||||||
#### udp_fragment
|
#### udp_fragment
|
||||||
|
|
||||||
启用 UDP 分段。
|
启用 UDP 分段。
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
"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,6 +25,7 @@
|
|||||||
| `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 |
|
||||||
@@ -42,6 +44,14 @@ 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,6 +5,7 @@
|
|||||||
"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,
|
||||||
@@ -23,6 +24,7 @@
|
|||||||
| `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` 启用时 |
|
||||||
@@ -43,6 +45,14 @@
|
|||||||
|
|
||||||
启用 TCP Fast Open。
|
启用 TCP Fast Open。
|
||||||
|
|
||||||
|
#### tcp_multi_path
|
||||||
|
|
||||||
|
!!! warning ""
|
||||||
|
|
||||||
|
需要 Go 1.21。
|
||||||
|
|
||||||
|
启用 TCP Multi Path。
|
||||||
|
|
||||||
#### udp_fragment
|
#### udp_fragment
|
||||||
|
|
||||||
启用 UDP 分段。
|
启用 UDP 分段。
|
||||||
|
|||||||
@@ -10,7 +10,8 @@
|
|||||||
"protocol": "smux",
|
"protocol": "smux",
|
||||||
"max_connections": 4,
|
"max_connections": 4,
|
||||||
"min_streams": 4,
|
"min_streams": 4,
|
||||||
"max_streams": 0
|
"max_streams": 0,
|
||||||
|
"padding": false
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -28,8 +29,9 @@ Multiplex protocol.
|
|||||||
|----------|------------------------------------|
|
|----------|------------------------------------|
|
||||||
| smux | https://github.com/xtaci/smux |
|
| smux | https://github.com/xtaci/smux |
|
||||||
| yamux | https://github.com/hashicorp/yamux |
|
| yamux | https://github.com/hashicorp/yamux |
|
||||||
|
| h2mux | https://golang.org/x/net/http2 |
|
||||||
|
|
||||||
SMux is used by default.
|
h2mux is used by default.
|
||||||
|
|
||||||
#### max_connections
|
#### max_connections
|
||||||
|
|
||||||
@@ -48,3 +50,12 @@ Conflict with `max_streams`.
|
|||||||
Maximum multiplexed streams in a connection before opening a new connection.
|
Maximum multiplexed streams in a connection before opening a new connection.
|
||||||
|
|
||||||
Conflict with `max_connections` and `min_streams`.
|
Conflict with `max_connections` and `min_streams`.
|
||||||
|
|
||||||
|
#### padding
|
||||||
|
|
||||||
|
!!! info
|
||||||
|
|
||||||
|
Requires sing-box server version 1.3-beta9 or later.
|
||||||
|
|
||||||
|
Enable padding.
|
||||||
|
|
||||||
|
|||||||
@@ -28,8 +28,9 @@
|
|||||||
|-------|------------------------------------|
|
|-------|------------------------------------|
|
||||||
| smux | https://github.com/xtaci/smux |
|
| smux | https://github.com/xtaci/smux |
|
||||||
| yamux | https://github.com/hashicorp/yamux |
|
| yamux | https://github.com/hashicorp/yamux |
|
||||||
|
| h2mux | https://golang.org/x/net/http2 |
|
||||||
|
|
||||||
默认使用 SMux。
|
默认使用 h2mux。
|
||||||
|
|
||||||
#### max_connections
|
#### max_connections
|
||||||
|
|
||||||
@@ -47,4 +48,13 @@
|
|||||||
|
|
||||||
在打开新连接之前,连接中的最大多路复用流数量。
|
在打开新连接之前,连接中的最大多路复用流数量。
|
||||||
|
|
||||||
与 `max_connections` 和 `min_streams` 冲突。
|
与 `max_connections` 和 `min_streams` 冲突。
|
||||||
|
|
||||||
|
#### padding
|
||||||
|
|
||||||
|
!!! info
|
||||||
|
|
||||||
|
需要 sing-box 服务器版本 1.3-beta9 或更高。
|
||||||
|
|
||||||
|
启用填充。
|
||||||
|
|
||||||
|
|||||||
106
docs/examples/fakeip.md
Normal file
106
docs/examples/fakeip.md
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
```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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
106
docs/examples/fakeip.zh.md
Normal file
106
docs/examples/fakeip.zh.md
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
```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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -8,3 +8,5 @@ Configuration examples for sing-box.
|
|||||||
* [Shadowsocks](./shadowsocks)
|
* [Shadowsocks](./shadowsocks)
|
||||||
* [ShadowTLS](./shadowtls)
|
* [ShadowTLS](./shadowtls)
|
||||||
* [Clash API](./clash-api)
|
* [Clash API](./clash-api)
|
||||||
|
* [WireGuard Direct](./wireguard-direct)
|
||||||
|
* [FakeIP](./fakeip)
|
||||||
|
|||||||
@@ -8,3 +8,5 @@ sing-box 的配置示例。
|
|||||||
* [Shadowsocks](./shadowsocks)
|
* [Shadowsocks](./shadowsocks)
|
||||||
* [ShadowTLS](./shadowtls)
|
* [ShadowTLS](./shadowtls)
|
||||||
* [Clash API](./clash-api)
|
* [Clash API](./clash-api)
|
||||||
|
* [WireGuard Direct](./wireguard-direct)
|
||||||
|
* [FakeIP](./fakeip)
|
||||||
|
|||||||
90
docs/examples/wireguard-direct.md
Normal file
90
docs/examples/wireguard-direct.md
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
# WireGuard Direct
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"dns": {
|
||||||
|
"servers": [
|
||||||
|
{
|
||||||
|
"tag": "google",
|
||||||
|
"address": "tls://8.8.8.8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "local",
|
||||||
|
"address": "223.5.5.5",
|
||||||
|
"detour": "direct"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"geoip": "cn",
|
||||||
|
"server": "direct"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"reverse_mapping": true
|
||||||
|
},
|
||||||
|
"inbounds": [
|
||||||
|
{
|
||||||
|
"type": "tun",
|
||||||
|
"tag": "tun",
|
||||||
|
"inet4_address": "172.19.0.1/30",
|
||||||
|
"auto_route": true,
|
||||||
|
"sniff": true,
|
||||||
|
"stack": "system"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outbounds": [
|
||||||
|
{
|
||||||
|
"type": "wireguard",
|
||||||
|
"tag": "wg",
|
||||||
|
"server": "127.0.0.1",
|
||||||
|
"server_port": 2345,
|
||||||
|
"local_address": [
|
||||||
|
"172.19.0.1/128"
|
||||||
|
],
|
||||||
|
"private_key": "KLTnpPY03pig/WC3zR8U7VWmpANHPFh2/4pwICGJ5Fk=",
|
||||||
|
"peer_public_key": "uvNabcamf6Rs0vzmcw99jsjTJbxo6eWGOykSY66zsUk="
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "dns",
|
||||||
|
"tag": "dns"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "direct",
|
||||||
|
"tag": "direct"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "block",
|
||||||
|
"tag": "block"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"route": {
|
||||||
|
"ip_rules": [
|
||||||
|
{
|
||||||
|
"port": 53,
|
||||||
|
"action": "return"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"geoip": "cn",
|
||||||
|
"geosite": "cn",
|
||||||
|
"action": "return"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "direct",
|
||||||
|
"outbound": "wg"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"protocol": "dns",
|
||||||
|
"outbound": "dns"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"geoip": "cn",
|
||||||
|
"geosite": "cn",
|
||||||
|
"outbound": "direct"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"auto_detect_interface": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
19
docs/faq/fakeip.md
Normal file
19
docs/faq/fakeip.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# FakeIP
|
||||||
|
|
||||||
|
FakeIP refers to a type of behavior in a program that simultaneously hijacks both DNS and connection requests. It
|
||||||
|
responds to DNS requests with virtual results and restores mapping when accepting connections.
|
||||||
|
|
||||||
|
#### Advantage
|
||||||
|
|
||||||
|
*
|
||||||
|
|
||||||
|
#### Limitation
|
||||||
|
|
||||||
|
* Its mechanism breaks applications that depend on returning correct remote addresses.
|
||||||
|
* Only A and AAAA (IP) requests are supported, which may break applications that rely on other requests.
|
||||||
|
|
||||||
|
#### Recommendation
|
||||||
|
|
||||||
|
* Enable `dns.independent_cache` unless you always resolve FakeIP domains remotely.
|
||||||
|
* 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.
|
||||||
18
docs/faq/fakeip.zh.md
Normal file
18
docs/faq/fakeip.zh.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# FakeIP
|
||||||
|
|
||||||
|
FakeIP 是指同时劫持 DNS 和连接请求的程序中的一种行为。它通过虚拟结果响应 DNS 请求,在接受连接时恢复映射。
|
||||||
|
|
||||||
|
#### 优点
|
||||||
|
|
||||||
|
*
|
||||||
|
|
||||||
|
#### 限制
|
||||||
|
|
||||||
|
* 它的机制会破坏依赖于返回正确远程地址的应用程序。
|
||||||
|
* 仅支持 A 和 AAAA(IP)请求,这可能会破坏依赖于其他请求的应用程序。
|
||||||
|
|
||||||
|
#### 建议
|
||||||
|
|
||||||
|
* 启用 `dns.independent_cache` 除非您始终远程解析 FakeIP 域。
|
||||||
|
* 如果使用 tun,请确保 tun 路由中包含 FakeIP 地址范围。
|
||||||
|
* 启用 `experimental.clash_api.store_fakeip` 以持久化 FakeIP 记录,或者使用 `dns.rules.rewrite_ttl` 避免程序重启后在 DNS 被缓存的环境中丢失记录。
|
||||||
@@ -11,12 +11,6 @@ it doesn't fit, because it compromises performance or design clarity, or because
|
|||||||
If it bothers you that sing-box is missing feature X, please forgive us and investigate the features that sing-box does
|
If it bothers you that sing-box is missing feature X, please forgive us and investigate the features that sing-box does
|
||||||
have. You might find that they compensate in interesting ways for the lack of X.
|
have. You might find that they compensate in interesting ways for the lack of X.
|
||||||
|
|
||||||
#### Fake IP
|
|
||||||
|
|
||||||
Fake IP (also called Fake DNS) is an invasive and imperfect DNS solution that breaks expected behavior, causes DNS leaks
|
|
||||||
and makes many software unusable. It is recommended by some software that lacks DNS processing and caching, but sing-box
|
|
||||||
does not need this.
|
|
||||||
|
|
||||||
#### Naive outbound
|
#### Naive outbound
|
||||||
|
|
||||||
NaïveProxy's main function is chromium's network stack, and it makes no sense to implement only its transport protocol.
|
NaïveProxy's main function is chromium's network stack, and it makes no sense to implement only its transport protocol.
|
||||||
|
|||||||
@@ -9,11 +9,6 @@
|
|||||||
|
|
||||||
如果 sing-box 缺少功能 X 让您感到困扰,请原谅我们并调查 sing-box 确实有的功能。 您可能会发现它们以有趣的方式弥补了 X 的缺失。
|
如果 sing-box 缺少功能 X 让您感到困扰,请原谅我们并调查 sing-box 确实有的功能。 您可能会发现它们以有趣的方式弥补了 X 的缺失。
|
||||||
|
|
||||||
#### Fake IP
|
|
||||||
|
|
||||||
Fake IP(也称 Fake DNS)是一种侵入性和不完善的 DNS 解决方案,它打破了预期的行为,导致 DNS 泄漏并使许多软件无法使用。
|
|
||||||
一些缺乏 DNS 处理和缓存的软件推荐使用它,但 sing-box 不需要。
|
|
||||||
|
|
||||||
#### Naive 出站
|
#### Naive 出站
|
||||||
|
|
||||||
NaïveProxy 的主要功能是 chromium 的网络栈,仅实现它的传输协议是舍本逐末的。
|
NaïveProxy 的主要功能是 chromium 的网络栈,仅实现它的传输协议是舍本逐末的。
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ 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,6 +9,7 @@
|
|||||||
#### 下载
|
#### 下载
|
||||||
|
|
||||||
* [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,11 +5,12 @@ Experimental iOS client for sing-box.
|
|||||||
#### Requirements
|
#### Requirements
|
||||||
|
|
||||||
* iOS 15.0+
|
* iOS 15.0+
|
||||||
* macOS 12.0+ with Apple Silicon
|
* An Apple account outside of mainland China
|
||||||
|
|
||||||
#### Download
|
#### Download
|
||||||
|
|
||||||
* [TestFlight](https://testflight.apple.com/join/c6ylui2j)
|
* [AppStore](https://apps.apple.com/us/app/sing-box/id6451272673)
|
||||||
|
* [TestFlight](https://testflight.apple.com/join/AcqO44FH)
|
||||||
|
|
||||||
#### Note
|
#### Note
|
||||||
|
|
||||||
|
|||||||
@@ -5,11 +5,12 @@
|
|||||||
#### 要求
|
#### 要求
|
||||||
|
|
||||||
* iOS 15.0+
|
* iOS 15.0+
|
||||||
* macOS 12.0+ with Apple Silicon
|
* 一个非中国大陆地区的 Apple 账号
|
||||||
|
|
||||||
#### 下载
|
#### 下载
|
||||||
|
|
||||||
* [TestFlight](https://testflight.apple.com/join/c6ylui2j)
|
* [AppStore](https://apps.apple.com/us/app/sing-box/id6451272673)
|
||||||
|
* [TestFlight](https://testflight.apple.com/join/AcqO44FH)
|
||||||
|
|
||||||
#### 注意事项
|
#### 注意事项
|
||||||
|
|
||||||
|
|||||||
29
docs/installation/clients/sfm.md
Normal file
29
docs/installation/clients/sfm.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# 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.
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user