Compare commits

..

1 Commits

Author SHA1 Message Date
世界
9092139e73 Initialize L3 routing support 2023-03-22 01:36:17 +08:00
250 changed files with 1902 additions and 5866 deletions

View File

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

View File

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

View File

@@ -117,6 +117,8 @@ nfpms:
dst: /etc/systemd/system/sing-box@.service
- src: LICENSE
dst: /usr/share/licenses/sing-box/LICENSE
scripts:
postremove: release/config/postremove.sh
source:
enabled: false
name_template: '{{ .ProjectName }}-{{ .Version }}.source'

View File

@@ -9,7 +9,7 @@ RUN set -ex \
&& apk add git build-base \
&& export COMMIT=$(git rev-parse --short HEAD) \
&& 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_quic,with_wireguard,with_reality_server,with_acme \
-o /go/bin/sing-box \
-ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" -s -w -buildid=" \
./cmd/sing-box

View File

@@ -11,7 +11,4 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
In addition, no derivative work may use the name or imply association
with this application without prior consent.
along with this program. If not, see <http://www.gnu.org/licenses/>.

View File

@@ -77,20 +77,13 @@ test_stdio:
go mod tidy && \
go test -v -tags "$(TAGS_TEST),force_stdio" .
android:
go run ./cmd/internal/build_libbox -target android
ios:
go run ./cmd/internal/build_libbox -target ios
lib:
go run ./cmd/internal/build_libbox -target android
go run ./cmd/internal/build_libbox -target ios
go run ./cmd/internal/build_libbox
lib_install:
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/gobind@v0.0.0-20230413023804-244d7ff07035
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.0.0-20221130124640-349ebaa752ca
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.0.0-20221130124640-349ebaa752ca
clean:
rm -rf bin dist sing-box

View File

@@ -8,10 +8,6 @@ The universal proxy platform.
https://sing-box.sagernet.org
## Support
https://community.sagernet.org/c/sing-box/
## License
```
@@ -29,7 +25,4 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
In addition, no derivative work may use the name or imply association
with this application without prior consent.
```

View File

@@ -13,7 +13,6 @@ type ClashServer interface {
PreStarter
Mode() string
StoreSelected() bool
StoreFakeIP() bool
CacheFile() ClashCacheFile
HistoryStorage() *urltest.HistoryStorage
RoutedConnection(ctx context.Context, conn net.Conn, metadata InboundContext, matchedRule Rule) (net.Conn, Tracker)
@@ -23,7 +22,6 @@ type ClashServer interface {
type ClashCacheFile interface {
LoadSelected(group string) string
StoreSelected(group string, selected string) error
FakeIPStorage
}
type Tracker interface {
@@ -35,11 +33,6 @@ type OutboundGroup interface {
All() []string
}
type URLTestGroup interface {
OutboundGroup
URLTest(ctx context.Context, url string) (map[string]uint16, error)
}
func OutboundTag(detour Outbound) string {
if group, isGroup := detour.(OutboundGroup); isGroup {
return group.Now()

View File

@@ -1,23 +0,0 @@
package adapter
import (
"net/netip"
"github.com/sagernet/sing-dns"
)
type FakeIPStore interface {
Service
Contains(address netip.Addr) bool
Create(domain string, strategy dns.DomainStrategy) (netip.Addr, error)
Lookup(address netip.Addr) (string, bool)
Reset() error
}
type FakeIPStorage interface {
FakeIPMetadata() *FakeIPMetadata
FakeIPSaveMetadata(metadata *FakeIPMetadata) error
FakeIPStore(address netip.Addr, domain string) error
FakeIPLoad(address netip.Addr) (string, bool)
FakeIPReset() error
}

View File

@@ -1,50 +0,0 @@
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
}

View File

@@ -4,7 +4,7 @@ import (
"context"
"net"
"github.com/sagernet/sing-tun"
tun "github.com/sagernet/sing-tun"
N "github.com/sagernet/sing/common/network"
)

View File

@@ -21,8 +21,6 @@ type Router interface {
Outbound(tag string) (Outbound, bool)
DefaultOutbound(network string) Outbound
FakeIPStore() FakeIPStore
RouteConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error
RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
RouteIPConnection(ctx context.Context, conn tun.RouteContext, metadata InboundContext) tun.RouteAction
@@ -37,7 +35,6 @@ type Router interface {
LookupDefault(ctx context.Context, domain string) ([]netip.Addr, error)
InterfaceFinder() control.InterfaceFinder
UpdateInterfaces() error
DefaultInterface() string
AutoDetectInterface() bool
AutoDetectInterfaceFunc() control.Func
@@ -84,7 +81,6 @@ type Rule interface {
type DNSRule interface {
Rule
DisableCache() bool
RewriteTTL() *uint32
}
type IPRule interface {

150
box.go
View File

@@ -9,6 +9,7 @@ import (
"time"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/experimental"
"github.com/sagernet/sing-box/experimental/libbox/platform"
"github.com/sagernet/sing-box/inbound"
@@ -30,25 +31,18 @@ type Box struct {
outbounds []adapter.Outbound
logFactory log.Factory
logger log.ContextLogger
logFile *os.File
preServices map[string]adapter.Service
postServices map[string]adapter.Service
done chan struct{}
}
type Options struct {
option.Options
Context context.Context
PlatformInterface platform.Interface
}
func New(options Options) (*Box, error) {
ctx := options.Context
if ctx == nil {
ctx = context.Background()
}
func New(ctx context.Context, options option.Options, platformInterface platform.Interface) (*Box, error) {
createdAt := time.Now()
experimentalOptions := common.PtrValueOrDefault(options.Experimental)
applyDebugOptions(common.PtrValueOrDefault(experimentalOptions.Debug))
var needClashAPI bool
var needV2RayAPI bool
if experimentalOptions.ClashAPI != nil && experimentalOptions.ClashAPI.ExternalController != "" {
@@ -57,21 +51,60 @@ func New(options Options) (*Box, error) {
if experimentalOptions.V2RayAPI != nil && experimentalOptions.V2RayAPI.Listen != "" {
needV2RayAPI = true
}
var defaultLogWriter io.Writer
if options.PlatformInterface != nil {
defaultLogWriter = io.Discard
}
logFactory, err := log.New(log.Options{
Context: ctx,
Options: common.PtrValueOrDefault(options.Log),
Observable: needClashAPI,
DefaultWriter: defaultLogWriter,
BaseTime: createdAt,
PlatformWriter: options.PlatformInterface,
})
if err != nil {
return nil, E.Cause(err, "create log factory")
logOptions := common.PtrValueOrDefault(options.Log)
var logFactory log.Factory
var observableLogFactory log.ObservableFactory
var logFile *os.File
var logWriter io.Writer
if logOptions.Disabled {
observableLogFactory = log.NewNOPFactory()
logFactory = observableLogFactory
} else {
switch logOptions.Output {
case "":
if platformInterface != nil {
logWriter = io.Discard
} else {
logWriter = os.Stdout
}
case "stderr":
logWriter = os.Stderr
case "stdout":
logWriter = os.Stdout
default:
var err error
logFile, err = os.OpenFile(C.BasePath(logOptions.Output), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
return nil, err
}
logWriter = logFile
}
logFormatter := log.Formatter{
BaseTime: createdAt,
DisableColors: logOptions.DisableColor || logFile != nil,
DisableTimestamp: !logOptions.Timestamp && logFile != nil,
FullTimestamp: logOptions.Timestamp,
TimestampFormat: "-0700 2006-01-02 15:04:05",
}
if needClashAPI {
observableLogFactory = log.NewObservableFactory(logFormatter, logWriter, platformInterface)
logFactory = observableLogFactory
} else {
logFactory = log.NewFactory(logFormatter, logWriter, platformInterface)
}
if logOptions.Level != "" {
logLevel, err := log.ParseLevel(logOptions.Level)
if err != nil {
return nil, E.Cause(err, "parse log level")
}
logFactory.SetLevel(logLevel)
} else {
logFactory.SetLevel(log.LevelTrace)
}
}
router, err := route.NewRouter(
ctx,
logFactory,
@@ -79,7 +112,7 @@ func New(options Options) (*Box, error) {
common.PtrValueOrDefault(options.DNS),
common.PtrValueOrDefault(options.NTP),
options.Inbounds,
options.PlatformInterface,
platformInterface,
)
if err != nil {
return nil, E.Cause(err, "parse route options")
@@ -99,7 +132,7 @@ func New(options Options) (*Box, error) {
router,
logFactory.NewLogger(F.ToString("inbound/", inboundOptions.Type, "[", tag, "]")),
inboundOptions,
options.PlatformInterface,
platformInterface,
)
if err != nil {
return nil, E.Cause(err, "parse inbound[", i, "]")
@@ -118,7 +151,6 @@ func New(options Options) (*Box, error) {
ctx,
router,
logFactory.NewLogger(F.ToString("outbound/", outboundOptions.Type, "[", tag, "]")),
tag,
outboundOptions)
if err != nil {
return nil, E.Cause(err, "parse outbound[", i, "]")
@@ -126,7 +158,7 @@ func New(options Options) (*Box, error) {
outbounds = append(outbounds, out)
}
err = router.Initialize(inbounds, outbounds, func() adapter.Outbound {
out, oErr := outbound.New(ctx, router, logFactory.NewLogger("outbound/direct"), "direct", option.Outbound{Type: "direct", Tag: "default"})
out, oErr := outbound.New(ctx, router, logFactory.NewLogger("outbound/direct"), option.Outbound{Type: "direct", Tag: "default"})
common.Must(oErr)
outbounds = append(outbounds, out)
return out
@@ -134,16 +166,10 @@ func New(options Options) (*Box, error) {
if err != nil {
return nil, err
}
if options.PlatformInterface != nil {
err = options.PlatformInterface.Initialize(ctx, router)
if err != nil {
return nil, E.Cause(err, "initialize platform interface")
}
}
preServices := make(map[string]adapter.Service)
postServices := make(map[string]adapter.Service)
if needClashAPI {
clashServer, err := experimental.NewClashServer(ctx, router, logFactory.(log.ObservableFactory), common.PtrValueOrDefault(options.Experimental.ClashAPI))
clashServer, err := experimental.NewClashServer(router, observableLogFactory, common.PtrValueOrDefault(options.Experimental.ClashAPI))
if err != nil {
return nil, E.Cause(err, "create clash api server")
}
@@ -165,6 +191,7 @@ func New(options Options) (*Box, error) {
createdAt: createdAt,
logFactory: logFactory,
logger: logFactory.Logger(),
logFile: logFile,
preServices: preServices,
postServices: postServices,
done: make(chan struct{}),
@@ -211,21 +238,21 @@ func (s *Box) Start() error {
func (s *Box) preStart() error {
for serviceName, service := range s.preServices {
s.logger.Trace("pre-start ", serviceName)
s.logger.Trace("pre-starting ", serviceName)
err := adapter.PreStart(service)
if err != nil {
return E.Cause(err, "pre-starting ", serviceName)
return E.Cause(err, "pre-start ", 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, "]")
var tag string
if out.Tag() == "" {
tag = F.ToString(i)
} else {
tag = out.Tag()
}
s.logger.Trace("initializing outbound ", tag)
err := starter.Start()
if err != nil {
return E.Cause(err, "initialize outbound/", out.Type(), "[", tag, "]")
@@ -254,17 +281,17 @@ func (s *Box) start() error {
} else {
tag = in.Tag()
}
s.logger.Trace("initializing inbound/", in.Type(), "[", tag, "]")
s.logger.Trace("initializing inbound ", tag)
err = in.Start()
if err != nil {
return E.Cause(err, "initialize inbound/", in.Type(), "[", tag, "]")
}
}
for serviceName, service := range s.postServices {
s.logger.Trace("starting ", service)
s.logger.Trace("start ", serviceName)
err = service.Start()
if err != nil {
return E.Cause(err, "start ", serviceName)
return E.Cause(err, "starting ", serviceName)
}
}
return nil
@@ -279,21 +306,33 @@ func (s *Box) Close() error {
}
var errors error
for serviceName, service := range s.postServices {
s.logger.Trace("closing ", serviceName)
errors = E.Append(errors, service.Close(), func(err error) error {
s.logger.Trace("closing ", serviceName)
return E.Cause(err, "close ", serviceName)
})
}
for i, in := range s.inbounds {
s.logger.Trace("closing inbound/", in.Type(), "[", i, "]")
var tag string
if in.Tag() == "" {
tag = F.ToString(i)
} else {
tag = in.Tag()
}
s.logger.Trace("closing inbound ", tag)
errors = E.Append(errors, in.Close(), func(err error) error {
return E.Cause(err, "close inbound/", in.Type(), "[", i, "]")
})
}
for i, out := range s.outbounds {
s.logger.Trace("closing outbound/", out.Type(), "[", i, "]")
var tag string
if out.Tag() == "" {
tag = F.ToString(i)
} else {
tag = out.Tag()
}
s.logger.Trace("closing outbound ", tag)
errors = E.Append(errors, common.Close(out), func(err error) error {
return E.Cause(err, "close outbound/", out.Type(), "[", i, "]")
return E.Cause(err, "close inbound/", out.Type(), "[", i, "]")
})
}
s.logger.Trace("closing router")
@@ -308,12 +347,17 @@ func (s *Box) Close() error {
return E.Cause(err, "close ", serviceName)
})
}
s.logger.Trace("closing log factory")
s.logger.Trace("closing logger")
if err := common.Close(s.logFactory); err != nil {
errors = E.Append(errors, err, func(err error) error {
return E.Cause(err, "close log factory")
})
}
if s.logFile != nil {
errors = E.Append(errors, s.logFile.Close(), func(err error) error {
return E.Cause(err, "close log file")
})
}
return errors
}

View File

@@ -31,10 +31,7 @@ func check() error {
return err
}
ctx, cancel := context.WithCancel(context.Background())
instance, err := box.New(box.Options{
Context: ctx,
Options: options,
})
instance, err := box.New(ctx, options, nil)
if err == nil {
instance.Close()
}

View File

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

View File

@@ -10,7 +10,6 @@ import (
"sort"
"strings"
"syscall"
"time"
"github.com/sagernet/sing-box"
"github.com/sagernet/sing-box/common/badjsonmerge"
@@ -128,10 +127,7 @@ func create() (*box.Box, context.CancelFunc, error) {
options.Log.DisableColor = true
}
ctx, cancel := context.WithCancel(context.Background())
instance, err := box.New(box.Options{
Context: ctx,
Options: options,
})
instance, err := box.New(ctx, options, nil)
if err != nil {
cancel()
return nil, nil, E.Cause(err, "create service")
@@ -178,10 +174,7 @@ func run() error {
}
}
cancel()
closeCtx, closed := context.WithCancel(context.Background())
go closeMonitor(closeCtx)
instance.Close()
closed()
if osSignal != syscall.SIGHUP {
return nil
}
@@ -189,13 +182,3 @@ func run() error {
}
}
}
func closeMonitor(ctx context.Context) {
time.Sleep(3 * time.Second)
select {
case <-ctx.Done():
return
default:
}
log.Fatal("sing-box did not close!")
}

View File

@@ -1,6 +1,8 @@
package main
import (
"context"
"github.com/sagernet/sing-box"
E "github.com/sagernet/sing/common/exceptions"
N "github.com/sagernet/sing/common/network"
@@ -25,7 +27,7 @@ func createPreStartedClient() (*box.Box, error) {
if err != nil {
return nil, err
}
instance, err := box.New(box.Options{Options: options})
instance, err := box.New(context.Background(), options, nil)
if err != nil {
return nil, E.Cause(err, "create service")
}

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

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

View File

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

View File

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

View File

@@ -21,7 +21,7 @@ func TestMergeJSON(t *testing.T) {
{
Type: C.RuleTypeDefault,
DefaultOptions: option.DefaultRule{
Network: []string{N.NetworkTCP},
Network: N.NetworkTCP,
Outbound: "direct",
},
},
@@ -42,7 +42,7 @@ func TestMergeJSON(t *testing.T) {
{
Type: C.RuleTypeDefault,
DefaultOptions: option.DefaultRule{
Network: []string{N.NetworkUDP},
Network: N.NetworkUDP,
Outbound: "direct",
},
},

View File

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

View File

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

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

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

View File

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

View File

@@ -1,114 +0,0 @@
package badversion
import (
"strconv"
"strings"
F "github.com/sagernet/sing/common/format"
)
type Version struct {
Major int
Minor int
Patch int
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 == "beta" && anotherVersion.PreReleaseIdentifier == "alpha" {
return true
} else if v.PreReleaseIdentifier == "alpha" && anotherVersion.PreReleaseIdentifier == "beta" {
return false
}
if v.PreReleaseVersion > anotherVersion.PreReleaseVersion {
return true
} else if v.PreReleaseVersion < anotherVersion.PreReleaseVersion {
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.PreReleaseIdentifier = 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
}

View File

@@ -1,17 +0,0 @@
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
}

View File

@@ -1,18 +0,0 @@
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")))
}

View File

@@ -0,0 +1,48 @@
package canceler
import (
"context"
"time"
)
type Instance struct {
ctx context.Context
cancelFunc context.CancelFunc
timer *time.Timer
timeout time.Duration
}
func New(ctx context.Context, cancelFunc context.CancelFunc, timeout time.Duration) *Instance {
instance := &Instance{
ctx,
cancelFunc,
time.NewTimer(timeout),
timeout,
}
go instance.wait()
return instance
}
func (i *Instance) Update() bool {
if !i.timer.Stop() {
return false
}
if !i.timer.Reset(i.timeout) {
return false
}
return true
}
func (i *Instance) wait() {
select {
case <-i.timer.C:
case <-i.ctx.Done():
}
i.Close()
}
func (i *Instance) Close() error {
i.timer.Stop()
i.cancelFunc()
return nil
}

49
common/canceler/packet.go Normal file
View File

@@ -0,0 +1,49 @@
package canceler
import (
"context"
"time"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/buf"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
type PacketConn struct {
N.PacketConn
instance *Instance
}
func NewPacketConn(ctx context.Context, conn N.PacketConn, timeout time.Duration) (context.Context, N.PacketConn) {
ctx, cancel := context.WithCancel(ctx)
instance := New(ctx, cancel, timeout)
return ctx, &PacketConn{conn, instance}
}
func (c *PacketConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) {
destination, err = c.PacketConn.ReadPacket(buffer)
if err == nil {
c.instance.Update()
}
return
}
func (c *PacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
err := c.PacketConn.WritePacket(buffer, destination)
if err == nil {
c.instance.Update()
}
return err
}
func (c *PacketConn) Close() error {
return common.Close(
c.PacketConn,
c.instance,
)
}
func (c *PacketConn) Upstream() any {
return c.PacketConn
}

View File

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

View File

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

View File

@@ -14,16 +14,10 @@ var (
)
func Count() int {
if !Enabled {
return 0
}
return openConnection.Len()
}
func List() []io.Closer {
if !Enabled {
return nil
}
connAccess.RLock()
defer connAccess.RUnlock()
connList := make([]io.Closer, 0, openConnection.Len())
@@ -34,9 +28,6 @@ func List() []io.Closer {
}
func Close() {
if !Enabled {
return
}
connAccess.Lock()
defer connAccess.Unlock()
for element := openConnection.Front(); element != nil; element = element.Next() {

View File

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

View File

@@ -9,7 +9,6 @@ import (
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-dns"
"github.com/sagernet/sing/common/bufio"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
@@ -69,11 +68,11 @@ func (d *ResolveDialer) ListenPacket(ctx context.Context, destination M.Socksadd
if err != nil {
return nil, err
}
conn, destinationAddress, err := N.ListenSerial(ctx, d.dialer, destination, addresses)
conn, err := N.ListenSerial(ctx, d.dialer, destination, addresses)
if err != nil {
return nil, err
}
return bufio.NewNATPacketConn(bufio.NewPacketConn(conn), M.SocksaddrFrom(destinationAddress, destination.Port), destination), nil
return NewResolvePacketConn(ctx, d.router, d.strategy, conn), nil
}
func (d *ResolveDialer) Upstream() any {

View File

@@ -0,0 +1,84 @@
package dialer
import (
"context"
"net"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-dns"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/buf"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
func NewResolvePacketConn(ctx context.Context, router adapter.Router, strategy dns.DomainStrategy, conn net.PacketConn) N.NetPacketConn {
if udpConn, ok := conn.(*net.UDPConn); ok {
return &ResolveUDPConn{udpConn, ctx, router, strategy}
} else {
return &ResolvePacketConn{conn, ctx, router, strategy}
}
}
type ResolveUDPConn struct {
*net.UDPConn
ctx context.Context
router adapter.Router
strategy dns.DomainStrategy
}
func (w *ResolveUDPConn) ReadPacket(buffer *buf.Buffer) (M.Socksaddr, error) {
n, addr, err := w.ReadFromUDPAddrPort(buffer.FreeBytes())
if err != nil {
return M.Socksaddr{}, err
}
buffer.Truncate(n)
return M.SocksaddrFromNetIP(addr), nil
}
func (w *ResolveUDPConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
defer buffer.Release()
if destination.IsFqdn() {
addresses, err := w.router.Lookup(w.ctx, destination.Fqdn, w.strategy)
if err != nil {
return err
}
return common.Error(w.UDPConn.WriteToUDPAddrPort(buffer.Bytes(), M.SocksaddrFrom(addresses[0], destination.Port).AddrPort()))
}
return common.Error(w.UDPConn.WriteToUDPAddrPort(buffer.Bytes(), destination.AddrPort()))
}
func (w *ResolveUDPConn) Upstream() any {
return w.UDPConn
}
type ResolvePacketConn struct {
net.PacketConn
ctx context.Context
router adapter.Router
strategy dns.DomainStrategy
}
func (w *ResolvePacketConn) ReadPacket(buffer *buf.Buffer) (M.Socksaddr, error) {
_, addr, err := buffer.ReadPacketFrom(w)
if err != nil {
return M.Socksaddr{}, err
}
return M.SocksaddrFromNet(addr), err
}
func (w *ResolvePacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
defer buffer.Release()
if destination.IsFqdn() {
addresses, err := w.router.Lookup(w.ctx, destination.Fqdn, w.strategy)
if err != nil {
return err
}
return common.Error(w.WriteTo(buffer.Bytes(), M.SocksaddrFrom(addresses[0], destination.Port).UDPAddr()))
}
return common.Error(w.WriteTo(buffer.Bytes(), destination.UDPAddr()))
}
func (w *ResolvePacketConn) Upstream() any {
return w.PacketConn
}

View File

@@ -124,10 +124,6 @@ func (c *slowOpenConn) LazyHeadroom() bool {
return c.conn == nil
}
func (c *slowOpenConn) NeedHandshake() bool {
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)

View File

@@ -28,10 +28,9 @@ type Client struct {
maxConnections int
minStreams int
maxStreams int
paddingEnabled bool
}
func NewClient(ctx context.Context, dialer N.Dialer, protocol Protocol, maxConnections int, minStreams int, maxStreams int, paddingEnabled bool) (*Client, error) {
func NewClient(ctx context.Context, dialer N.Dialer, protocol Protocol, maxConnections int, minStreams int, maxStreams int) *Client {
return &Client{
ctx: ctx,
dialer: dialer,
@@ -39,11 +38,10 @@ func NewClient(ctx context.Context, dialer N.Dialer, protocol Protocol, maxConne
maxConnections: maxConnections,
minStreams: minStreams,
maxStreams: maxStreams,
paddingEnabled: paddingEnabled,
}, nil
}
}
func NewClientWithOptions(ctx context.Context, dialer N.Dialer, options option.MultiplexOptions) (*Client, error) {
func NewClientWithOptions(ctx context.Context, dialer N.Dialer, options option.MultiplexOptions) (N.Dialer, error) {
if !options.Enabled {
return nil, nil
}
@@ -54,7 +52,7 @@ func NewClientWithOptions(ctx context.Context, dialer N.Dialer, options option.M
if err != nil {
return nil, err
}
return NewClient(ctx, dialer, protocol, options.MaxConnections, options.MinStreams, options.MaxStreams, 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) {
@@ -122,16 +120,17 @@ func (c *Client) offer() (abstractSession, error) {
sessions = append(sessions, element.Value)
element = element.Next()
}
session := common.MinBy(common.Filter(sessions, abstractSession.CanTakeNewRequest), abstractSession.NumStreams)
if session == nil {
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 len(sessions) >= c.maxConnections || numStreams < c.minStreams {
if sLen >= c.maxConnections || numStreams < c.minStreams {
return session, nil
}
} else {
@@ -147,19 +146,10 @@ func (c *Client) offerNew() (abstractSession, error) {
if err != nil {
return nil, err
}
var version byte
if c.paddingEnabled {
version = Version1
if vectorisedWriter, isVectorised := bufio.CreateVectorisedWriter(conn); isVectorised {
conn = &vectorisedProtocolConn{protocolConn{Conn: conn, protocol: c.protocol}, vectorisedWriter}
} else {
version = Version0
}
conn = newProtocolConn(conn, Request{
Version: version,
Protocol: c.protocol,
PaddingEnabled: c.paddingEnabled,
})
if c.paddingEnabled {
conn = newPaddingConn(conn)
conn = &protocolConn{Conn: conn, protocol: c.protocol}
}
session, err := c.protocol.newClient(conn)
if err != nil {
@@ -169,15 +159,6 @@ func (c *Client) offerNew() (abstractSession, error) {
return session, nil
}
func (c *Client) Reset() {
c.access.Lock()
defer c.access.Unlock()
for _, session := range c.connections.Array() {
session.Close()
}
c.connections.Init()
}
func (c *Client) Close() error {
c.access.Lock()
defer c.access.Unlock()
@@ -224,7 +205,7 @@ func (c *ClientConn) Write(b []byte) (n int, err error) {
Network: N.NetworkTCP,
Destination: c.destination,
}
_buffer := buf.StackNewSize(streamRequestLen(request) + len(b))
_buffer := buf.StackNewSize(requestLen(request) + len(b))
defer common.KeepAlive(_buffer)
buffer := common.Dup(_buffer)
defer buffer.Release()
@@ -268,10 +249,6 @@ func (c *ClientConn) WriterReplaceable() bool {
return c.requestWrite
}
func (c *ClientConn) NeedAdditionalReadDeadline() bool {
return true
}
func (c *ClientConn) Upstream() any {
return c.Conn
}
@@ -318,7 +295,7 @@ func (c *ClientPacketConn) writeRequest(payload []byte) (n int, err error) {
Network: N.NetworkUDP,
Destination: c.destination,
}
rLen := streamRequestLen(request)
rLen := requestLen(request)
if len(payload) > 0 {
rLen += 2 + len(payload)
}
@@ -400,10 +377,6 @@ 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
}
@@ -440,11 +413,7 @@ func (c *ClientPacketAddrConn) ReadFrom(p []byte) (n int, addr net.Addr, err err
if err != nil {
return
}
if destination.IsFqdn() {
addr = destination
} else {
addr = destination.UDPAddr()
}
addr = destination.UDPAddr()
var length uint16
err = binary.Read(c.ExtendedConn, binary.BigEndian, &length)
if err != nil {
@@ -463,7 +432,7 @@ func (c *ClientPacketAddrConn) writeRequest(payload []byte, destination M.Socksa
Destination: c.destination,
PacketAddr: true,
}
rLen := streamRequestLen(request)
rLen := requestLen(request)
if len(payload) > 0 {
rLen += M.SocksaddrSerializer.AddrPortLen(destination) + 2 + len(payload)
}
@@ -545,10 +514,6 @@ func (c *ClientPacketAddrConn) FrontHeadroom() int {
return 2 + M.MaxSocksaddrLength
}
func (c *ClientPacketAddrConn) NeedAdditionalReadDeadline() bool {
return true
}
func (c *ClientPacketAddrConn) Upstream() any {
return c.ExtendedConn
}

View File

@@ -1,235 +0,0 @@
package mux
import (
"context"
"crypto/tls"
"io"
"net"
"net/http"
"net/url"
"os"
"time"
"github.com/sagernet/sing-box/transport/v2rayhttp"
"github.com/sagernet/sing/common/atomic"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/bufio"
E "github.com/sagernet/sing/common/exceptions"
N "github.com/sagernet/sing/common/network"
"golang.org/x/net/http2"
)
const idleTimeout = 30 * time.Second
var _ abstractSession = (*H2MuxServerSession)(nil)
type H2MuxServerSession struct {
server http2.Server
active atomic.Int32
conn net.Conn
inbound chan net.Conn
done chan struct{}
}
func NewH2MuxServer(conn net.Conn) *H2MuxServerSession {
session := &H2MuxServerSession{
conn: conn,
inbound: make(chan net.Conn),
done: make(chan struct{}),
server: http2.Server{
IdleTimeout: idleTimeout,
},
}
go func() {
session.server.ServeConn(conn, &http2.ServeConnOpts{
Handler: session,
})
_ = session.Close()
}()
return session
}
func (s *H2MuxServerSession) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
s.active.Add(1)
defer s.active.Add(-1)
writer.WriteHeader(http.StatusOK)
conn := newHTTP2Wrapper(&v2rayhttp.ServerHTTPConn{
HTTP2Conn: v2rayhttp.NewHTTPConn(request.Body, writer),
Flusher: writer.(http.Flusher),
})
s.inbound <- conn
select {
case <-conn.done:
case <-s.done:
}
}
func (s *H2MuxServerSession) Open() (net.Conn, error) {
return nil, os.ErrInvalid
}
func (s *H2MuxServerSession) Accept() (net.Conn, error) {
select {
case conn := <-s.inbound:
return conn, nil
case <-s.done:
return nil, os.ErrClosed
}
}
func (s *H2MuxServerSession) NumStreams() int {
return int(s.active.Load())
}
func (s *H2MuxServerSession) Close() error {
select {
case <-s.done:
default:
close(s.done)
}
return s.conn.Close()
}
func (s *H2MuxServerSession) IsClosed() bool {
select {
case <-s.done:
return true
default:
return false
}
}
func (s *H2MuxServerSession) CanTakeNewRequest() bool {
return false
}
type h2MuxConnWrapper struct {
N.ExtendedConn
done chan struct{}
}
func newHTTP2Wrapper(conn net.Conn) *h2MuxConnWrapper {
return &h2MuxConnWrapper{
ExtendedConn: bufio.NewExtendedConn(conn),
done: make(chan struct{}),
}
}
func (w *h2MuxConnWrapper) Write(p []byte) (n int, err error) {
select {
case <-w.done:
return 0, net.ErrClosed
default:
}
return w.ExtendedConn.Write(p)
}
func (w *h2MuxConnWrapper) WriteBuffer(buffer *buf.Buffer) error {
select {
case <-w.done:
return net.ErrClosed
default:
}
return w.ExtendedConn.WriteBuffer(buffer)
}
func (w *h2MuxConnWrapper) Close() error {
select {
case <-w.done:
default:
close(w.done)
}
return w.ExtendedConn.Close()
}
func (w *h2MuxConnWrapper) Upstream() any {
return w.ExtendedConn
}
var _ abstractSession = (*H2MuxClientSession)(nil)
type H2MuxClientSession struct {
transport *http2.Transport
clientConn *http2.ClientConn
done chan struct{}
}
func NewH2MuxClient(conn net.Conn) (*H2MuxClientSession, error) {
session := &H2MuxClientSession{
transport: &http2.Transport{
DialTLSContext: func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) {
return conn, nil
},
ReadIdleTimeout: idleTimeout,
},
done: make(chan struct{}),
}
session.transport.ConnPool = session
clientConn, err := session.transport.NewClientConn(conn)
if err != nil {
return nil, err
}
session.clientConn = clientConn
return session, nil
}
func (s *H2MuxClientSession) GetClientConn(req *http.Request, addr string) (*http2.ClientConn, error) {
return s.clientConn, nil
}
func (s *H2MuxClientSession) MarkDead(conn *http2.ClientConn) {
s.Close()
}
func (s *H2MuxClientSession) Open() (net.Conn, error) {
pipeInReader, pipeInWriter := io.Pipe()
request := &http.Request{
Method: http.MethodConnect,
Body: pipeInReader,
URL: &url.URL{Scheme: "https", Host: "localhost"},
}
conn := v2rayhttp.NewLateHTTPConn(pipeInWriter)
go func() {
response, err := s.transport.RoundTrip(request)
if err != nil {
conn.Setup(nil, err)
} else if response.StatusCode != 200 {
response.Body.Close()
conn.Setup(nil, E.New("unexpected status: ", response.StatusCode, " ", response.Status))
} else {
conn.Setup(response.Body, nil)
}
}()
return conn, nil
}
func (s *H2MuxClientSession) Accept() (net.Conn, error) {
return nil, os.ErrInvalid
}
func (s *H2MuxClientSession) NumStreams() int {
return s.clientConn.State().StreamsActive
}
func (s *H2MuxClientSession) Close() error {
select {
case <-s.done:
default:
close(s.done)
}
return s.clientConn.Close()
}
func (s *H2MuxClientSession) IsClosed() bool {
select {
case <-s.done:
return true
default:
}
return s.clientConn.State().Closed
}
func (s *H2MuxClientSession) CanTakeNewRequest() bool {
return s.clientConn.CanTakeNewRequest()
}

View File

@@ -1,240 +0,0 @@
package mux
import (
"encoding/binary"
"io"
"math/rand"
"net"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/bufio"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/rw"
)
const kFirstPaddings = 16
type paddingConn struct {
N.ExtendedConn
writer N.VectorisedWriter
readPadding int
writePadding int
readRemaining int
paddingRemaining int
}
func newPaddingConn(conn net.Conn) net.Conn {
writer, isVectorised := bufio.CreateVectorisedWriter(conn)
if isVectorised {
return &vectorisedPaddingConn{
paddingConn{
ExtendedConn: bufio.NewExtendedConn(conn),
writer: bufio.NewVectorisedWriter(conn),
},
writer,
}
} else {
return &paddingConn{
ExtendedConn: bufio.NewExtendedConn(conn),
writer: bufio.NewVectorisedWriter(conn),
}
}
}
func (c *paddingConn) Read(p []byte) (n int, err error) {
if c.readRemaining > 0 {
if len(p) > c.readRemaining {
p = p[:c.readRemaining]
}
n, err = c.ExtendedConn.Read(p)
if err != nil {
return
}
c.readRemaining -= n
return
}
if c.paddingRemaining > 0 {
err = rw.SkipN(c.ExtendedConn, c.paddingRemaining)
if err != nil {
return
}
c.paddingRemaining = 0
}
if c.readPadding < kFirstPaddings {
var paddingHdr []byte
if len(p) >= 4 {
paddingHdr = p[:4]
} else {
_paddingHdr := make([]byte, 4)
defer common.KeepAlive(_paddingHdr)
paddingHdr = common.Dup(_paddingHdr)
}
_, err = io.ReadFull(c.ExtendedConn, paddingHdr)
if err != nil {
return
}
originalDataSize := int(binary.BigEndian.Uint16(paddingHdr[:2]))
paddingLen := int(binary.BigEndian.Uint16(paddingHdr[2:]))
if len(p) > originalDataSize {
p = p[:originalDataSize]
}
n, err = c.ExtendedConn.Read(p)
if err != nil {
return
}
c.readPadding++
c.readRemaining = originalDataSize - n
c.paddingRemaining = paddingLen
return
}
return c.ExtendedConn.Read(p)
}
func (c *paddingConn) Write(p []byte) (n int, err error) {
for pLen := len(p); pLen > 0; {
var data []byte
if pLen > 65535 {
data = p[:65535]
p = p[65535:]
pLen -= 65535
} else {
data = p
pLen = 0
}
var writeN int
writeN, err = c.write(data)
n += writeN
if err != nil {
break
}
}
return n, err
}
func (c *paddingConn) write(p []byte) (n int, err error) {
if c.writePadding < kFirstPaddings {
paddingLen := 256 + rand.Intn(512)
_buffer := buf.StackNewSize(4 + len(p) + paddingLen)
defer common.KeepAlive(_buffer)
buffer := common.Dup(_buffer)
defer buffer.Release()
header := buffer.Extend(4)
binary.BigEndian.PutUint16(header[:2], uint16(len(p)))
binary.BigEndian.PutUint16(header[2:], uint16(paddingLen))
common.Must1(buffer.Write(p))
buffer.Extend(paddingLen)
_, err = c.ExtendedConn.Write(buffer.Bytes())
if err == nil {
n = len(p)
}
c.writePadding++
return
}
return c.ExtendedConn.Write(p)
}
func (c *paddingConn) ReadBuffer(buffer *buf.Buffer) error {
p := buffer.FreeBytes()
if c.readRemaining > 0 {
if len(p) > c.readRemaining {
p = p[:c.readRemaining]
}
n, err := c.ExtendedConn.Read(p)
if err != nil {
return err
}
c.readRemaining -= n
buffer.Truncate(n)
return nil
}
if c.paddingRemaining > 0 {
err := rw.SkipN(c.ExtendedConn, c.paddingRemaining)
if err != nil {
return err
}
c.paddingRemaining = 0
}
if c.readPadding < kFirstPaddings {
var paddingHdr []byte
if len(p) >= 4 {
paddingHdr = p[:4]
} else {
_paddingHdr := make([]byte, 4)
defer common.KeepAlive(_paddingHdr)
paddingHdr = common.Dup(_paddingHdr)
}
_, err := io.ReadFull(c.ExtendedConn, paddingHdr)
if err != nil {
return err
}
originalDataSize := int(binary.BigEndian.Uint16(paddingHdr[:2]))
paddingLen := int(binary.BigEndian.Uint16(paddingHdr[2:]))
if len(p) > originalDataSize {
p = p[:originalDataSize]
}
n, err := c.ExtendedConn.Read(p)
if err != nil {
return err
}
c.readPadding++
c.readRemaining = originalDataSize - n
c.paddingRemaining = paddingLen
buffer.Truncate(n)
return nil
}
return c.ExtendedConn.ReadBuffer(buffer)
}
func (c *paddingConn) WriteBuffer(buffer *buf.Buffer) error {
if c.writePadding < kFirstPaddings {
bufferLen := buffer.Len()
if bufferLen > 65535 {
return common.Error(c.Write(buffer.Bytes()))
}
paddingLen := 256 + rand.Intn(512)
header := buffer.ExtendHeader(4)
binary.BigEndian.PutUint16(header[:2], uint16(bufferLen))
binary.BigEndian.PutUint16(header[2:], uint16(paddingLen))
buffer.Extend(paddingLen)
c.writePadding++
}
return c.ExtendedConn.WriteBuffer(buffer)
}
func (c *paddingConn) FrontHeadroom() int {
return 4 + 256 + 1024
}
type vectorisedPaddingConn struct {
paddingConn
writer N.VectorisedWriter
}
func (c *vectorisedPaddingConn) WriteVectorised(buffers []*buf.Buffer) error {
if c.writePadding < kFirstPaddings {
bufferLen := buf.LenMulti(buffers)
if bufferLen > 65535 {
defer buf.ReleaseMulti(buffers)
for _, buffer := range buffers {
_, err := c.Write(buffer.Bytes())
if err != nil {
return err
}
}
return nil
}
paddingLen := 256 + rand.Intn(512)
header := buf.NewSize(4)
common.Must(
binary.Write(header, binary.BigEndian, uint16(bufferLen)),
binary.Write(header, binary.BigEndian, uint16(paddingLen)),
)
c.writePadding++
padding := buf.NewSize(paddingLen)
padding.Extend(paddingLen)
buffers = append(append([]*buf.Buffer{header}, buffers...), padding)
}
return c.writer.WriteVectorised(buffers)
}

View File

@@ -3,7 +3,6 @@ package mux
import (
"encoding/binary"
"io"
"math/rand"
"net"
C "github.com/sagernet/sing-box/constant"
@@ -26,7 +25,6 @@ var Destination = M.Socksaddr{
const (
ProtocolSMux Protocol = iota
ProtocolYAMux
ProtocolH2Mux
)
type Protocol byte
@@ -37,10 +35,8 @@ func ParseProtocol(name string) (Protocol, error) {
return ProtocolSMux, nil
case "yamux":
return ProtocolYAMux, nil
case "h2mux":
return ProtocolH2Mux, nil
default:
return ProtocolSMux, E.New("unknown multiplex protocol: ", name)
return ProtocolYAMux, E.New("unknown multiplex protocol: ", name)
}
}
@@ -53,13 +49,7 @@ func (p Protocol) newServer(conn net.Conn) (abstractSession, error) {
}
return &smuxSession{session}, nil
case ProtocolYAMux:
session, err := yamux.Server(conn, yaMuxConfig())
if err != nil {
return nil, err
}
return &yamuxSession{session}, nil
case ProtocolH2Mux:
return NewH2MuxServer(conn), nil
return yamux.Server(conn, yaMuxConfig())
default:
panic("unknown protocol")
}
@@ -74,13 +64,7 @@ func (p Protocol) newClient(conn net.Conn) (abstractSession, error) {
}
return &smuxSession{session}, nil
case ProtocolYAMux:
session, err := yamux.Client(conn, yaMuxConfig())
if err != nil {
return nil, err
}
return &yamuxSession{session}, nil
case ProtocolH2Mux:
return NewH2MuxClient(conn)
return yamux.Client(conn, yaMuxConfig())
default:
panic("unknown protocol")
}
@@ -106,22 +90,17 @@ func (p Protocol) String() string {
return "smux"
case ProtocolYAMux:
return "yamux"
case ProtocolH2Mux:
return "h2mux"
default:
return "unknown"
}
}
const (
Version0 = iota
Version1
version0 = 0
)
type Request struct {
Version byte
Protocol Protocol
PaddingEnabled bool
Protocol Protocol
}
func ReadRequest(reader io.Reader) (*Request, error) {
@@ -129,60 +108,22 @@ func ReadRequest(reader io.Reader) (*Request, error) {
if err != nil {
return nil, err
}
if version < Version0 || version > Version1 {
if version != version0 {
return nil, E.New("unsupported version: ", version)
}
protocol, err := rw.ReadByte(reader)
if err != nil {
return nil, err
}
var paddingEnabled bool
if version == Version1 {
err = binary.Read(reader, binary.BigEndian, &paddingEnabled)
if err != nil {
return nil, err
}
if paddingEnabled {
var paddingLen uint16
err = binary.Read(reader, binary.BigEndian, &paddingLen)
if err != nil {
return nil, err
}
err = rw.SkipN(reader, int(paddingLen))
if err != nil {
return nil, err
}
}
if protocol > byte(ProtocolYAMux) {
return nil, E.New("unsupported protocol: ", protocol)
}
return &Request{Version: version, Protocol: Protocol(protocol), PaddingEnabled: paddingEnabled}, nil
return &Request{Protocol: Protocol(protocol)}, nil
}
func EncodeRequest(request Request, payload []byte) *buf.Buffer {
var requestLen int
requestLen += 2
var paddingLen uint16
if request.Version == Version1 {
requestLen += 1
if request.PaddingEnabled {
requestLen += 2
paddingLen = uint16(256 + rand.Intn(512))
requestLen += int(paddingLen)
}
}
buffer := buf.NewSize(requestLen + len(payload))
common.Must(
buffer.WriteByte(request.Version),
buffer.WriteByte(byte(request.Protocol)),
)
if request.Version == Version1 {
common.Must(binary.Write(buffer, binary.BigEndian, request.PaddingEnabled))
if request.PaddingEnabled {
common.Must(binary.Write(buffer, binary.BigEndian, paddingLen))
buffer.Extend(int(paddingLen))
}
}
common.Must1(buffer.Write(payload))
return buffer
func EncodeRequest(buffer *buf.Buffer, request Request) {
buffer.WriteByte(version0)
buffer.WriteByte(byte(request.Protocol))
}
const (
@@ -219,7 +160,7 @@ func ReadStreamRequest(reader io.Reader) (*StreamRequest, error) {
return &StreamRequest{network, destination, udpAddr}, nil
}
func streamRequestLen(request StreamRequest) int {
func requestLen(request StreamRequest) int {
var rLen int
rLen += 1 // version
rLen += 2 // flags

View File

@@ -22,9 +22,6 @@ func NewConnection(ctx context.Context, router adapter.Router, errorHandler E.Ha
if err != nil {
return err
}
if request.PaddingEnabled {
conn = newPaddingConn(conn)
}
session, err := request.Protocol.newServer(conn)
if err != nil {
return err
@@ -134,10 +131,6 @@ func (c *ServerConn) FrontHeadroom() int {
return 0
}
func (c *ServerConn) NeedAdditionalReadDeadline() bool {
return true
}
func (c *ServerConn) Upstream() any {
return c.ExtendedConn
}
@@ -190,10 +183,6 @@ func (c *ServerPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksad
return c.ExtendedConn.WriteBuffer(buffer)
}
func (c *ServerPacketConn) NeedAdditionalReadDeadline() bool {
return true
}
func (c *ServerPacketConn) Upstream() any {
return c.ExtendedConn
}
@@ -256,10 +245,6 @@ func (c *ServerPacketAddrConn) WritePacket(buffer *buf.Buffer, destination M.Soc
return c.ExtendedConn.WriteBuffer(buffer)
}
func (c *ServerPacketAddrConn) NeedAdditionalReadDeadline() bool {
return true
}
func (c *ServerPacketAddrConn) Upstream() any {
return c.ExtendedConn
}

View File

@@ -4,12 +4,11 @@ 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"
"github.com/hashicorp/yamux"
)
type abstractSession interface {
@@ -18,7 +17,6 @@ type abstractSession interface {
NumStreams() int
Close() error
IsClosed() bool
CanTakeNewRequest() bool
}
var _ abstractSession = (*smuxSession)(nil)
@@ -35,49 +33,25 @@ func (s *smuxSession) Accept() (net.Conn, error) {
return s.AcceptStream()
}
func (s *smuxSession) CanTakeNewRequest() bool {
return true
}
type yamuxSession struct {
*yamux.Session
}
func (y *yamuxSession) CanTakeNewRequest() bool {
return true
}
type protocolConn struct {
net.Conn
request Request
protocol Protocol
protocolWritten bool
}
func newProtocolConn(conn net.Conn, request Request) net.Conn {
writer, isVectorised := bufio.CreateVectorisedWriter(conn)
if isVectorised {
return &vectorisedProtocolConn{
protocolConn{
Conn: conn,
request: request,
},
writer,
}
} else {
return &protocolConn{
Conn: conn,
request: request,
}
}
}
func (c *protocolConn) Write(p []byte) (n int, err error) {
if c.protocolWritten {
return c.Conn.Write(p)
}
buffer := EncodeRequest(c.request, p)
_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())
buffer.Release()
if err == nil {
n--
}
@@ -98,14 +72,20 @@ func (c *protocolConn) Upstream() any {
type vectorisedProtocolConn struct {
protocolConn
writer N.VectorisedWriter
N.VectorisedWriter
}
func (c *vectorisedProtocolConn) WriteVectorised(buffers []*buf.Buffer) error {
if c.protocolWritten {
return c.writer.WriteVectorised(buffers)
return c.VectorisedWriter.WriteVectorised(buffers)
}
c.protocolWritten = true
buffer := EncodeRequest(c.request, nil)
return c.writer.WriteVectorised(append([]*buf.Buffer{buffer}, buffers...))
_buffer := buf.StackNewSize(2)
defer common.KeepAlive(_buffer)
buffer := common.Dup(_buffer)
defer buffer.Release()
EncodeRequest(buffer, Request{
Protocol: c.protocol,
})
return c.VectorisedWriter.WriteVectorised(append([]*buf.Buffer{buffer}, buffers...))
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -124,9 +124,8 @@ func (e *RealityClientConfig) ClientHandshake(ctx context.Context, conn net.Conn
binary.BigEndian.PutUint64(hello.SessionId, uint64(nowTime.Unix()))
hello.SessionId[0] = 1
hello.SessionId[1] = 8
hello.SessionId[2] = 1
binary.BigEndian.PutUint32(hello.SessionId[4:], uint32(time.Now().Unix()))
hello.SessionId[1] = 7
hello.SessionId[2] = 5
copy(hello.SessionId[8:], e.shortID[:])
if debug.Enabled {

View File

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

View File

@@ -50,9 +50,6 @@ func (s *HistoryStorage) StoreURLTestHistory(tag string, history *History) {
}
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)
if err != nil {
return

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

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

View File

@@ -3,13 +3,28 @@ package constant
import (
"os"
"path/filepath"
"strings"
"github.com/sagernet/sing/common/rw"
)
const dirName = "sing-box"
var resourcePaths []string
var (
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) {
name = os.ExpandEnv(name)

View File

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

View File

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

View File

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

View File

@@ -1,128 +1,3 @@
#### 1.3-beta9
* Improve multiplex **1**
* Fixes and improvements
*1*:
Added new `h2mux` multiplex protocol and `padding` multiplex option, see [Multiplex](/configuration/shared/multiplex).
#### 1.2.6
* Fix bugs and update dependencies
#### 1.3-beta8
* Fix `system` tun stack for ios
* Fix network monitor for android/ios
* Update VLESS and XUDP protocol **1**
* Fixes and improvements
*1:
This is an incompatible update for XUDP in VLESS if vision flow is enabled.
#### 1.3-beta7
* Add `path` and `headers` options for HTTP outbound
* Add multi-user support for Shadowsocks legacy AEAD inbound
* Fixes and improvements
#### 1.2.4
* Fixes and improvements
#### 1.3-beta6
* Fix WireGuard reconnect
* Perform URLTest recheck after network changes
* Fix bugs and update dependencies
#### 1.3-beta5
* Add Clash.Meta API compatibility for Clash API
* Download Yacd-meta by default if the specified Clash `external_ui` directory is empty
* Add path and headers option for HTTP outbound
* Fixes and improvements
#### 1.3-beta4
* Fix bugs
#### 1.3-beta2
* 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
* Introducing our [new Android client application](/installation/clients/sfa)
* Improve UDP domain destination NAT
* Update reality protocol
* Fix TTL calculation for DNS response
* Fix v2ray HTTP transport compatibility
* Fix bugs and update dependencies
#### 1.2.2
* Accept `any` outbound in dns rule **1**
* Fix bugs and update dependencies
*1*:
Now you can use the `any` outbound rule to match server address queries instead of filling in all server domains
to `domain` rule.
#### 1.2.1
* Fix missing default host in v2ray http transport`s request
* Flush DNS cache for macOS when tun start/close
* Fix tun's DNS hijacking compatibility with systemd-resolved
#### 1.2.0
* Fix bugs and update dependencies
Important changes since 1.1:
* Introducing our [new iOS client application](/installation/clients/sfi)
* Introducing [UDP over TCP protocol version 2](/configuration/shared/udp-over-tcp)
* Add [platform options](/configuration/inbound/tun#platform) for tun inbound
* Add [ShadowTLS protocol v3](https://github.com/ihciah/shadow-tls/blob/master/docs/protocol-v3-en.md)
* Add [VLESS server](/configuration/inbound/vless) and [vision](/configuration/outbound/vless#flow) support
* Add [reality TLS](/configuration/shared/tls) support
* Add [NTP service](/configuration/ntp)
* Add [DHCP DNS server](/configuration/dns/server) support
* Add SSH [host key validation](/configuration/outbound/ssh) support
* Add [query_type](/configuration/dns/rule) DNS rule item
* Add fallback support for v2ray transport
* Add custom TLS server support for http based v2ray transports
* Add health check support for http-based v2ray transports
* Add multiple configuration support
#### 1.2-rc1
* Fix bugs and update dependencies
#### 1.2-beta10
* Add multiple configuration support **1**
@@ -130,11 +5,9 @@ Important changes since 1.1:
*1*:
Now you can pass the parameter `--config` or `-c` multiple times, or use the new parameter `--config-directory` or `-C`
to load all configuration files in a directory.
Now you can pass the parameter `--config` or `-c` multiple times, or use the new parameter `--config-directory` or `-C` to load all configuration files in a directory.
Loaded configuration files are sorted by name. If you want to control the merge order, add a numeric prefix to the file
name.
Loaded configuration files are sorted by name. If you want to control the merge order, add a numeric prefix to the file name.
#### 1.1.7

View File

@@ -1,25 +0,0 @@
# 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.

View File

@@ -1,25 +0,0 @@
# 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 地址范围。

View File

@@ -10,9 +10,7 @@
"final": "",
"strategy": "",
"disable_cache": false,
"disable_expire": false,
"reverse_mapping": false,
"fakeip": {}
"disable_expire": false
}
}
@@ -24,7 +22,6 @@
|----------|--------------------------------|
| `server` | List of [DNS Server](./server) |
| `rules` | List of [DNS Rule](./rule) |
| `fakeip` | [FakeIP](./fakeip) |
#### final
@@ -46,15 +43,4 @@ Disable dns cache.
#### disable_expire
Disable dns cache expire.
#### 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.
Disable dns cache expire.

View File

@@ -10,9 +10,7 @@
"final": "",
"strategy": "",
"disable_cache": false,
"disable_expire": false,
"reverse_mapping": false,
"fakeip": {}
"disable_expire": false
}
}
@@ -45,14 +43,4 @@
#### disable_expire
禁用 DNS 缓存过期。
#### reverse_mapping
在响应 DNS 查询后存储 IP 地址的反向映射以为路由目的提供域名。
由于此过程依赖于应用程序在发出请求之前解析域名的行为,因此在 macOS 等 DNS 由系统代理和缓存的环境中可能会出现问题。
#### fakeip
[FakeIP](./fakeip) 设置。
禁用 DNS 缓存过期。

View File

@@ -84,16 +84,14 @@
"direct"
],
"server": "local",
"disable_cache": false,
"rewrite_ttl": 100
"disable_cache": false
},
{
"type": "logical",
"mode": "and",
"rules": [],
"server": "local",
"disable_cache": false,
"rewrite_ttl": 100
"disable_cache": false
}
]
}
@@ -234,8 +232,6 @@ Invert match result.
Match outbound.
`any` can be used as a value to match any outbound.
#### server
==Required==
@@ -246,10 +242,6 @@ Tag of the target dns server.
Disable cache and save cache in this query.
#### rewrite_ttl
Rewrite TTL in DNS responses.
### Logical Fields
#### type
@@ -262,4 +254,18 @@ Rewrite TTL in DNS responses.
#### rules
Included default rules.
Included default rules.
#### invert
Invert match result.
#### server
==Required==
Tag of the target dns server.
#### disable_cache
Disable cache and save cache in this query.

View File

@@ -231,8 +231,6 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
匹配出站。
`any` 可作为值用于匹配任意出站。
#### server
==必填==
@@ -243,10 +241,6 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
在此查询中禁用缓存。
#### rewrite_ttl
重写 DNS 回应中的 TTL。
### 逻辑字段
#### type
@@ -259,4 +253,18 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
#### rules
包括的默认规则。
包括的默认规则。
#### invert
反选匹配结果。
#### server
==必填==
目标 DNS 服务器的标签。
#### disable_cache
在此查询中禁用缓存。

View File

@@ -30,18 +30,17 @@ The tag of the dns server.
The address of the dns server.
| Protocol | Format |
|---------------------|-------------------------------|
| `System` | `local` |
| `TCP` | `tcp://1.0.0.1` |
| `UDP` | `8.8.8.8` `udp://8.8.4.4` |
| `TLS` | `tls://dns.google` |
| `HTTPS` | `https://1.1.1.1/dns-query` |
| `QUIC` | `quic://dns.adguard.com` |
| `HTTP3` | `h3://8.8.8.8/dns-query` |
| `RCode` | `rcode://refused` |
| `DHCP` | `dhcp://auto` or `dhcp://en0` |
| [FakeIP](./fakeip) | `fakeip` |
| Protocol | Format |
|----------|-------------------------------|
| `System` | `local` |
| `TCP` | `tcp://1.0.0.1` |
| `UDP` | `8.8.8.8` `udp://8.8.4.4` |
| `TLS` | `tls://dns.google` |
| `HTTPS` | `https://1.1.1.1/dns-query` |
| `QUIC` | `quic://dns.adguard.com` |
| `HTTP3` | `h3://8.8.8.8/dns-query` |
| `RCode` | `rcode://refused` |
| `DHCP` | `dhcp://auto` or `dhcp://en0` |
!!! warning ""

View File

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

View File

@@ -8,8 +8,6 @@
"clash_api": {
"external_controller": "127.0.0.1:9090",
"external_ui": "folder",
"external_ui_download_url": "",
"external_ui_download_detour": "",
"secret": "",
"default_mode": "rule",
"store_selected": false,
@@ -55,18 +53,6 @@ A relative path to the configuration directory or an absolute path to a
directory in which you put some static web resource. sing-box will then
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 for the RESTful API (optional)

View File

@@ -8,8 +8,6 @@
"clash_api": {
"external_controller": "127.0.0.1:9090",
"external_ui": "folder",
"external_ui_download_url": "",
"external_ui_download_detour": "",
"secret": "",
"default_mode": "rule",
"store_selected": false,
@@ -53,18 +51,6 @@ RESTful web API 监听地址。如果为空,则禁用 Clash API。
到静态网页资源目录的相对路径或绝对路径。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
RESTful API 的密钥(可选)

View File

@@ -40,8 +40,4 @@ No authentication required if empty.
Only supported on Linux, Android, Windows, and macOS.
!!! warning ""
To work on Android and iOS without privileges, use tun.platform.http_proxy instead.
Automatically set system proxy configuration when start and clean up when stop.

View File

@@ -40,8 +40,4 @@ HTTP 用户
仅支持 Linux、Android、Windows 和 macOS。
!!! warning ""
要在无特权的 Android 和 iOS 上工作,请改用 tun.platform.http_proxy。
启动时自动设置系统代理,停止时自动清理。

View File

@@ -37,8 +37,4 @@ No authentication required if empty.
Only supported on Linux, Android, Windows, and macOS.
!!! warning ""
To work on Android and iOS without privileges, use tun.platform.http_proxy instead.
Automatically set system proxy configuration when start and clean up when stop.
Automatically set system proxy configuration when start and clean up when stop.

View File

@@ -37,8 +37,4 @@ SOCKS 和 HTTP 用户
仅支持 Linux、Android、Windows 和 macOS。
!!! warning ""
要在无特权的 Android 和 iOS 上工作,请改用 tun.platform.http_proxy。
启动时自动设置系统代理,停止时自动清理。

View File

@@ -107,7 +107,8 @@ Enforce strict routing rules when `auto_route` is enabled:
* Let unsupported network unreachable
* Route all connections to tun
It prevents address leaks and makes DNS hijacking work on Android, but your device will not be accessible by others.
It prevents address leaks and makes DNS hijacking work on Android and Linux with systemd-resolved, but your device will
not be accessible by others.
*In Windows*:

View File

@@ -107,7 +107,7 @@ tun 接口的 IPv6 前缀。
* 让不支持的网络无法到达
* 将所有连接路由到 tun
它可以防止地址泄漏,并使 DNS 劫持在 Android 上工作,但你的设备将无法其他设备被访问。
它可以防止地址泄漏,并使 DNS 劫持在 Android 和使用 systemd-resolved 的 Linux 上工作,但你的设备将无法其他设备被访问。
*在 Windows 中*:

View File

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

View File

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

View File

@@ -37,10 +37,4 @@
#### tag
The tag of the outbound.
### Features
#### Outbounds that support IP connection
* `WireGuard`
The tag of the outbound.

View File

@@ -36,10 +36,4 @@
#### tag
出站的标签。
### 特性
#### 支持 IP 连接的出站
* `WireGuard`
出站的标签。

View File

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

View File

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

View File

@@ -13,18 +13,6 @@
"10.0.0.2/32"
],
"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=",
"pre_shared_key": "31aIhAPwktDGpH4JDhA8GNvjFXEf/a6+UaQRyOAiyfM=",
"reserved": [0, 0, 0],
@@ -48,13 +36,13 @@
#### server
==Required if multi-peer disabled==
==Required==
The server address.
#### server_port
==Required if multi-peer disabled==
==Required==
The server port.
@@ -87,25 +75,9 @@ wg genkey
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
==Required if multi-peer disabled==
==Required==
WireGuard peer public key.

View File

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

View File

@@ -7,7 +7,6 @@
"route": {
"geoip": {},
"geosite": {},
"ip_rules": [],
"rules": [],
"final": "",
"auto_detect_interface": false,
@@ -20,12 +19,11 @@
### 字段
| 键 | 格式 |
|------------|-------------------------|
| `geoip` | [GeoIP](./geoip) |
| `geosite` | [GeoSite](./geosite) |
| `ip_rules` | 一组 [IP 路由规则](./ip-rule) |
| `rules` | 一组 [路由规则](./rule) |
| 键 | 格式 |
|-----------|----------------------|
| `geoip` | [GeoIP](./geoip) |
| `geosite` | [GeoSite](./geosite) |
| `rules` | 一组 [路由规则](./rule) |
#### final
@@ -67,4 +65,4 @@
默认为出站连接设置路由标记。
如果设置了 `outbound.routing_mark` 设置,则不生效。
如果设置了 `outbound.routing_mark` 设置,则不生效。

View File

@@ -1,205 +0,0 @@
### 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.

View File

@@ -1,204 +0,0 @@
### 结构
```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
==必填==
包括的默认规则。

View File

@@ -9,9 +9,7 @@
"mixed-in"
],
"ip_version": 6,
"network": [
"tcp"
],
"network": "tcp",
"auth_user": [
"usera",
"userb"
@@ -246,12 +244,18 @@ Tag of the target outbound.
#### mode
==Required==
`and` or `or`
#### rules
Included default rules.
#### invert
Invert match result.
#### outbound
==Required==
Included default rules.
Tag of the target outbound.

View File

@@ -9,9 +9,7 @@
"mixed-in"
],
"ip_version": 6,
"network": [
"tcp"
],
"network": "tcp",
"auth_user": [
"usera",
"userb"
@@ -244,12 +242,18 @@
#### mode
==必填==
`and``or`
#### rules
包括的默认规则。
#### invert
反选匹配结果。
#### outbound
==必填==
包括的默认规则
目标出站的标签

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,9 +7,9 @@
#### Install
```shell
git clone -b main https://github.com/SagerNet/sing-box
git clone https://github.com/SagerNet/sing-box
cd sing-box
./release/local/install_go.sh # skip if you have golang already installed
./release/local/install_go.sh # skip if you have go1.19 already installed
./release/local/install.sh
```

View File

@@ -7,9 +7,9 @@
#### 安装
```shell
git clone -b main https://github.com/SagerNet/sing-box
git clone https://github.com/SagerNet/sing-box
cd sing-box
./release/local/install_go.sh # 如果已安装 golang 则跳过
./release/local/install_go.sh # 如果已安装 go1.19 则跳过
./release/local/install.sh
```

View File

@@ -23,10 +23,7 @@
"disable_cache": true
},
{
"outbound": "any",
"server": "local"
},
{
"domain": "mydomain.com",
"geosite": "cn",
"server": "local"
}

View File

@@ -1,90 +0,0 @@
# 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
}
}
```

View File

@@ -1,20 +0,0 @@
# 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
* Retrieve the requested domain in places like IP routing (L3) where traffic detection is not possible to assist with routing.
* Decrease an RTT on the first TCP request to a domain (the most common reason).
#### Limitation
* 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
* Do not use if you do not need L3 routing.
* 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.

View File

@@ -1,19 +0,0 @@
# FakeIP
FakeIP 是指同时劫持 DNS 和连接请求的程序中的一种行为。它通过虚拟结果响应 DNS 请求,在接受连接时恢复映射。
#### 优点
* 在像 L3 路由这样无法进行流量探测的地方检索所请求的域名,以协助路由。
* 减少对一个域的第一个 TCP 请求的 RTT这是最常见的原因
#### 限制
* 它的机制会破坏依赖于返回正确远程地址的应用程序。
* 仅支持 A 和 AAAAIP请求这可能会破坏依赖于其他请求的应用程序。
#### 建议
* 如果不需要 L3 路由,请勿使用。
* 如果使用 tun请确保 tun 路由中包含 FakeIP 地址范围。
* 启用 `experimental.clash_api.store_fakeip` 以持久化 FakeIP 记录,或者使用 `dns.rules.rewrite_ttl` 避免程序重启后在 DNS 被缓存的环境中丢失记录。

View File

@@ -11,6 +11,12 @@ 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
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
NaïveProxy's main function is chromium's network stack, and it makes no sense to implement only its transport protocol.

View File

@@ -9,6 +9,11 @@
如果 sing-box 缺少功能 X 让您感到困扰,请原谅我们并调查 sing-box 确实有的功能。 您可能会发现它们以有趣的方式弥补了 X 的缺失。
#### Fake IP
Fake IP也称 Fake DNS是一种侵入性和不完善的 DNS 解决方案,它打破了预期的行为,导致 DNS 泄漏并使许多软件无法使用。
一些缺乏 DNS 处理和缓存的软件推荐使用它,但 sing-box 不需要。
#### Naive 出站
NaïveProxy 的主要功能是 chromium 的网络栈,仅实现它的传输协议是舍本逐末的。

View File

@@ -9,6 +9,10 @@ the public internet.
`auto-route` cannot automatically hijack DNS requests when Android's `Private DNS` enabled or `strict_route` disabled.
##### on Linux
`auto-route` cannot automatically hijack DNS requests with `systemd-resolved` enabled and `strict_route` disabled.
#### System proxy
##### on Linux

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