Compare commits

..

39 Commits

Author SHA1 Message Date
世界
49f23cfb8a documentation: Bump version 2025-02-12 21:14:01 +08:00
xchacha20-poly1305
f2b507a551 Remove single quotes of raw Moziila certs 2025-02-12 21:03:43 +08:00
世界
5e1a15e99c Add Tailscale endpoint 2025-02-12 21:03:43 +08:00
世界
6ec2bda264 Bump Go to go1.24 2025-02-12 21:03:43 +08:00
世界
5154ec9c7d Build legacy binaries with latest Go 2025-02-12 21:03:43 +08:00
世界
a4aadbc24e Fix parsing legacy DNS servers 2025-02-12 17:58:30 +08:00
世界
971a2350ae documentation: Bump version 2025-02-12 12:26:18 +08:00
世界
cf3a2d49ed badjson: Fix merge objects 2025-02-12 12:26:03 +08:00
ReleTor
6502d789cd documentation: Fixes 2025-02-12 12:25:54 +08:00
世界
cb17c08db8 Fix crash in exchangeParallel 2025-02-12 12:25:54 +08:00
世界
7500b699e5 Fix match DNS rule for fqdn 2025-02-12 12:25:54 +08:00
世界
45a8a709e5 Fix WireGuard panic 2025-02-12 12:25:53 +08:00
世界
65d37cce85 Fix domain resolver for DNS server 2025-02-12 12:25:53 +08:00
世界
04cd4cbee1 documentation: Fix fakeip example 2025-02-12 12:25:53 +08:00
世界
e054deb55e documentation: Remove outdated icons 2025-02-12 12:25:53 +08:00
世界
3fe2fa6fcb documentation: Certificate store 2025-02-12 12:25:52 +08:00
世界
76cbe5fe5c documentation: TLS fragment 2025-02-12 12:25:52 +08:00
世界
3faaa0c0bb documentation: Outbound domain resolver 2025-02-12 12:25:51 +08:00
世界
df76db0fe2 documentation: Refactor DNS 2025-02-12 12:25:51 +08:00
世界
ff61980d80 Add certificate store 2025-02-12 12:25:51 +08:00
世界
ef75508286 Add TLS fragment support 2025-02-12 12:25:50 +08:00
世界
7cec397f8e refactor: Outbound domain resolver 2025-02-12 12:25:43 +08:00
世界
6ec57e5e7e refactor: DNS 2025-02-12 12:25:43 +08:00
世界
ff31c469a0 Override version 2025-02-11 15:55:15 +08:00
世界
fbe390268c Bump version 2025-02-11 01:32:14 +08:00
世界
07ac01dcb7 platform: Update NDK to r28 2025-02-11 01:32:14 +08:00
ReleTor
badfdb62cd documentation: Fixes 2025-02-11 01:32:14 +08:00
printfer
986a410b30 documentation: Fix migration links 2025-02-11 01:32:14 +08:00
世界
9db2d58545 Fix override address 2025-02-11 01:32:14 +08:00
世界
4eed46ac59 Fix respond ICMP echo 2025-02-10 15:12:10 +08:00
世界
abc38d1dab Fix udpnat2 crash 2025-02-10 15:11:26 +08:00
世界
8d6c4f1289 release: Skip testflight when another build in review 2025-02-06 12:02:47 +08:00
世界
a2d40eb8b8 Fix override UDP destination 2025-02-06 11:20:35 +08:00
世界
17b502bb4b Update dependencies 2025-02-06 09:08:52 +08:00
世界
a0d4421085 Update quic-go to v0.49.0 2025-02-06 08:50:21 +08:00
世界
0d443072d1 Fix panic in auto-redirect initialize 2025-02-06 08:49:25 +08:00
世界
c9fb99b799 Fix missing ENOTCONN in IsClosed check 2025-02-06 08:48:49 +08:00
世界
92d245ad04 Bump version 2025-02-05 09:59:52 +08:00
世界
0908627297 Fix crash on remote rule-set stop 2025-02-05 08:58:10 +08:00
117 changed files with 1891 additions and 7580 deletions

22
.github/setup_legacy_go.sh vendored Executable file
View File

@@ -0,0 +1,22 @@
#!/usr/bin/env bash
VERSION="1.23.6"
wget "https://dl.google.com/go/go${VERSION}.linux-amd64.tar.gz"
tar -xzf "go${VERSION}.linux-amd64.tar.gz"
mv go $HOME/go/go_legacy
cd $HOME/go/go_legacy
# modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557
# this patch file only works on golang1.23.x
# that means after golang1.24 release it must be changed
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.23/
# revert:
# 693def151adff1af707d82d28f55dba81ceb08e1: "crypto/rand,runtime: switch RtlGenRandom for ProcessPrng"
# 7c1157f9544922e96945196b47b95664b1e39108: "net: remove sysSocket fallback for Windows 7"
# 48042aa09c2f878c4faa576948b07fe625c4707a: "syscall: remove Windows 7 console handle workaround"
# a17d959debdb04cd550016a3501dd09d50cd62e7: "runtime: always use LoadLibraryEx to load system libraries"
curl https://github.com/MetaCubeX/go/commit/9ac42137ef6730e8b7daca016ece831297a1d75b.diff | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/21290de8a4c91408de7c2b5b68757b1e90af49dd.diff | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/6a31d3fa8e47ddabc10bd97bff10d9a85f4cfb76.diff | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/69e2eed6dd0f6d815ebf15797761c13f31213dd6.diff | patch --verbose -p 1

View File

@@ -46,7 +46,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.23
go-version: ^1.24
- name: Check input version
if: github.event_name == 'workflow_dispatch'
run: |-
@@ -112,7 +112,6 @@ jobs:
- name: darwin_amd64
goos: darwin
goarch: amd64
require_legacy_go: true
- name: android_arm64
goos: android
goarch: arm64
@@ -134,26 +133,23 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.23
go-version: ^1.24
- name: Cache legacy Go
if: matrix.require_legacy_go
id: cache-legacy-go
uses: actions/cache@v4
with:
path: |
~/go/go1.20.14
key: go120
~/go/go_legacy
key: go_legacy_1236
- name: Setup legacy Go
if: matrix.require_legacy_go && steps.cache-legacy-go.outputs.cache-hit != 'true'
run: |-
wget https://dl.google.com/go/go1.20.14.linux-amd64.tar.gz
tar -xzf go1.20.14.linux-amd64.tar.gz
mv go $HOME/go/go1.20.14
run: bash .github/setup_legacy_go.sh
- name: Setup Android NDK
if: matrix.goos == 'android'
uses: nttld/setup-ndk@v1
with:
ndk-version: r28-beta2
ndk-version: r28
local-cache: true
- name: Setup Goreleaser
uses: goreleaser/goreleaser-action@v6
@@ -219,12 +215,12 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.23
go-version: ^1.24
- name: Setup Android NDK
id: setup-ndk
uses: nttld/setup-ndk@v1
with:
ndk-version: r28-beta3
ndk-version: r28
- name: Setup OpenJDK
run: |-
sudo apt update && sudo apt install -y openjdk-17-jdk-headless
@@ -294,12 +290,12 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.23
go-version: ^1.24
- name: Setup Android NDK
id: setup-ndk
uses: nttld/setup-ndk@v1
with:
ndk-version: r28-beta3
ndk-version: r28
- name: Setup OpenJDK
run: |-
sudo apt update && sudo apt install -y openjdk-17-jdk-headless
@@ -392,7 +388,7 @@ jobs:
if: matrix.if
uses: actions/setup-go@v5
with:
go-version: ^1.23
go-version: ^1.24
- name: Setup Xcode stable
if: matrix.if && github.ref == 'refs/heads/main-next'
run: |-

View File

@@ -28,7 +28,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.23
go-version: ^1.24
- name: golangci-lint
uses: golangci/golangci-lint-action@v6
with:

View File

@@ -16,7 +16,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.23
go-version: ^1.24
- name: Extract signing key
run: |-
mkdir -p $HOME/.gnupg

View File

@@ -19,6 +19,7 @@ builds:
- with_reality_server
- with_acme
- with_clash_api
- with_tailscale
env:
- CGO_ENABLED=0
targets:

View File

@@ -21,6 +21,7 @@ builds:
- with_reality_server
- with_acme
- with_clash_api
- with_tailscale
env:
- CGO_ENABLED=0
targets:
@@ -49,14 +50,14 @@ builds:
- with_reality_server
- with_acme
- with_clash_api
- with_tailscale
env:
- CGO_ENABLED=0
- GOROOT={{ .Env.GOPATH }}/go1.20.14
tool: "{{ .Env.GOPATH }}/go1.20.14/bin/go"
- GOROOT={{ .Env.GOPATH }}/go_legacy
tool: "{{ .Env.GOPATH }}/go_legacy/bin/go"
targets:
- windows_amd64_v1
- windows_386
- darwin_amd64_v1
- id: android
<<: *template
env:
@@ -122,8 +123,8 @@ nfpms:
- deb
- rpm
- archlinux
# - apk
# - ipk
# - apk
# - ipk
priority: extra
contents:
- src: release/config/config.json

View File

@@ -1,4 +1,4 @@
FROM --platform=$BUILDPLATFORM golang:1.23-alpine AS builder
FROM --platform=$BUILDPLATFORM golang:1.24-alpine AS builder
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
COPY . /go/src/github.com/sagernet/sing-box
WORKDIR /go/src/github.com/sagernet/sing-box

View File

@@ -2,7 +2,8 @@ NAME = sing-box
COMMIT = $(shell git rev-parse --short HEAD)
TAGS_GO120 = with_gvisor,with_dhcp,with_wireguard,with_reality_server,with_clash_api,with_quic,with_utls
TAGS_GO121 = with_ech
TAGS ?= $(TAGS_GO118),$(TAGS_GO120),$(TAGS_GO121)
TAGS_GO123 = with_tailscale
TAGS ?= $(TAGS_GO118),$(TAGS_GO120),$(TAGS_GO121),$(TAGS_GO123)
TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_ech,with_utls,with_reality_server
GOHOSTOS = $(shell go env GOHOSTOS)
@@ -61,9 +62,6 @@ proto_install:
go install -v google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install -v google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
update_public_suffix:
go generate common/tlsfragment/public_suffix.go
update_certificates:
go run ./cmd/internal/update_certificates

View File

@@ -52,10 +52,6 @@ type CacheFile interface {
StoreGroupExpand(group string, expand bool) error
LoadRuleSet(tag string) *SavedBinary
SaveRuleSet(tag string, set *SavedBinary) error
LoadScript(tag string) *SavedBinary
SaveScript(tag string, script *SavedBinary) error
SurgePersistentStoreRead(key string) string
SurgePersistentStoreWrite(key string, value string) error
}
type SavedBinary struct {

View File

@@ -2,8 +2,6 @@ package adapter
import (
"context"
"crypto/tls"
"net/http"
"net/netip"
"time"
@@ -59,8 +57,6 @@ type InboundContext struct {
Domain string
Client string
SniffContext any
HTTPRequest *http.Request
ClientHello *tls.ClientHelloInfo
// cache
@@ -77,7 +73,6 @@ type InboundContext struct {
UDPTimeout time.Duration
TLSFragment bool
TLSFragmentFallbackDelay time.Duration
MITM *option.MITMRouteOptions
NetworkStrategy *C.NetworkStrategy
NetworkType []C.InterfaceType

View File

@@ -1,8 +1,6 @@
package adapter
import (
E "github.com/sagernet/sing/common/exceptions"
)
import E "github.com/sagernet/sing/common/exceptions"
type StartStage uint8
@@ -47,9 +45,6 @@ type LifecycleService interface {
func Start(stage StartStage, services ...Lifecycle) error {
for _, service := range services {
if service == nil {
continue
}
err := service.Start(stage)
if err != nil {
return err

View File

@@ -1,15 +0,0 @@
package adapter
import (
"context"
"crypto/x509"
"net"
N "github.com/sagernet/sing/common/network"
)
type MITMEngine interface {
Lifecycle
ExportCertificate() *x509.Certificate
NewConnection(ctx context.Context, this N.Dialer, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc)
}

View File

@@ -1,54 +0,0 @@
package adapter
import (
"context"
"net/http"
"sync"
"time"
)
type ScriptManager interface {
Lifecycle
Scripts() []Script
Script(name string) (Script, bool)
SurgeCache() *SurgeInMemoryCache
}
type SurgeInMemoryCache struct {
sync.RWMutex
Data map[string]string
}
type Script interface {
Type() string
Tag() string
StartContext(ctx context.Context, startContext *HTTPStartContext) error
PostStart() error
Close() error
}
type SurgeScript interface {
Script
ExecuteGeneric(ctx context.Context, scriptType string, timeout time.Duration, arguments []string) error
ExecuteHTTPRequest(ctx context.Context, timeout time.Duration, request *http.Request, body []byte, binaryBody bool, arguments []string) (*HTTPRequestScriptResult, error)
ExecuteHTTPResponse(ctx context.Context, timeout time.Duration, request *http.Request, response *http.Response, body []byte, binaryBody bool, arguments []string) (*HTTPResponseScriptResult, error)
}
type HTTPRequestScriptResult struct {
URL string
Headers http.Header
Body []byte
Response *HTTPRequestScriptResponse
}
type HTTPRequestScriptResponse struct {
Status int
Headers http.Header
Body []byte
}
type HTTPResponseScriptResult struct {
Status int
Headers http.Header
Body []byte
}

35
box.go
View File

@@ -23,11 +23,9 @@ import (
"github.com/sagernet/sing-box/experimental/cachefile"
"github.com/sagernet/sing-box/experimental/libbox/platform"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/mitm"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/protocol/direct"
"github.com/sagernet/sing-box/route"
"github.com/sagernet/sing-box/script"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
@@ -50,8 +48,6 @@ type Box struct {
dnsRouter *dns.Router
connection *route.ConnectionManager
router *route.Router
script *script.Manager
mitm adapter.MITMEngine //*mitm.Engine
services []adapter.LifecycleService
done chan struct{}
}
@@ -177,7 +173,7 @@ func New(options Options) (*Box, error) {
return nil, E.Cause(err, "initialize network manager")
}
service.MustRegister[adapter.NetworkManager](ctx, networkManager)
connectionManager := route.NewConnectionManager(ctx, logFactory.NewLogger("connection"))
connectionManager := route.NewConnectionManager(logFactory.NewLogger("connection"))
service.MustRegister[adapter.ConnectionManager](ctx, connectionManager)
router := route.NewRouter(ctx, logFactory, routeOptions, dnsOptions)
service.MustRegister[adapter.Router](ctx, router)
@@ -185,8 +181,8 @@ func New(options Options) (*Box, error) {
if err != nil {
return nil, E.Cause(err, "initialize router")
}
var timeService *tls.TimeServiceWrapper
ntpOptions := common.PtrValueOrDefault(options.NTP)
var timeService *tls.TimeServiceWrapper
if ntpOptions.Enabled {
timeService = new(tls.TimeServiceWrapper)
service.MustRegister[ntp.TimeService](ctx, timeService)
@@ -293,11 +289,6 @@ func New(options Options) (*Box, error) {
"local",
option.LocalDNSServerOptions{},
)))
scriptManager, err := script.NewManager(ctx, logFactory, options.Scripts)
if err != nil {
return nil, E.Cause(err, "initialize script manager")
}
service.MustRegister[adapter.ScriptManager](ctx, scriptManager)
if platformInterface != nil {
err = platformInterface.Initialize(networkManager)
if err != nil {
@@ -347,16 +338,6 @@ func New(options Options) (*Box, error) {
timeService.TimeService = ntpService
services = append(services, adapter.NewLifecycleService(ntpService, "ntp service"))
}
mitmOptions := common.PtrValueOrDefault(options.MITM)
var mitmEngine adapter.MITMEngine
if mitmOptions.Enabled {
engine, err := mitm.NewEngine(ctx, logFactory.NewLogger("mitm"), mitmOptions)
if err != nil {
return nil, E.Cause(err, "create MITM engine")
}
service.MustRegister[adapter.MITMEngine](ctx, engine)
mitmEngine = engine
}
return &Box{
network: networkManager,
endpoint: endpointManager,
@@ -366,8 +347,6 @@ func New(options Options) (*Box, error) {
dnsRouter: dnsRouter,
connection: connectionManager,
router: router,
script: scriptManager,
mitm: mitmEngine,
createdAt: createdAt,
logFactory: logFactory,
logger: logFactory.Logger(),
@@ -426,11 +405,11 @@ func (s *Box) preStart() error {
if err != nil {
return err
}
err = adapter.Start(adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.script, s.mitm, s.outbound, s.inbound, s.endpoint)
err = adapter.Start(adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint)
if err != nil {
return err
}
err = adapter.Start(adapter.StartStateStart, s.outbound, s.dnsTransport, s.dnsRouter, s.network, s.connection, s.router, s.script, s.mitm)
err = adapter.Start(adapter.StartStateStart, s.outbound, s.dnsTransport, s.dnsRouter, s.network, s.connection, s.router)
if err != nil {
return err
}
@@ -454,7 +433,7 @@ func (s *Box) start() error {
if err != nil {
return err
}
err = adapter.Start(adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.script, s.mitm, s.inbound, s.endpoint)
err = adapter.Start(adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.inbound, s.endpoint)
if err != nil {
return err
}
@@ -462,7 +441,7 @@ func (s *Box) start() error {
if err != nil {
return err
}
err = adapter.Start(adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.script, s.mitm, s.outbound, s.inbound, s.endpoint)
err = adapter.Start(adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint)
if err != nil {
return err
}
@@ -481,7 +460,7 @@ func (s *Box) Close() error {
close(s.done)
}
err := common.Close(
s.inbound, s.outbound, s.endpoint, s.mitm, s.script, s.router, s.connection, s.dnsRouter, s.dnsTransport, s.network,
s.inbound, s.outbound, s.endpoint, s.router, s.connection, s.dnsRouter, s.dnsTransport, s.network,
)
for _, lifecycleService := range s.services {
err = E.Append(err, lifecycleService.Close(), func(err error) error {

View File

@@ -58,7 +58,7 @@ func init() {
sharedFlags = append(sharedFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -s -w -buildid=")
debugFlags = append(debugFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag)
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_ech", "with_utls", "with_clash_api")
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_ech", "with_utls", "with_clash_api", "with_tailscale")
iosTags = append(iosTags, "with_dhcp", "with_low_memory", "with_conntrack")
debugTags = append(debugTags, "debug")
}

View File

@@ -48,7 +48,7 @@ func FindSDK() {
}
func findNDK() bool {
const fixedVersion = "28.0.12916984"
const fixedVersion = "28.0.13004108"
const versionFile = "source.properties"
if fixedPath := filepath.Join(androidSDKPath, "ndk", fixedVersion); rw.IsFile(filepath.Join(fixedPath, versionFile)) {
androidNDKPath = fixedPath

View File

@@ -46,21 +46,23 @@ func main() {
switch propPair[0] {
case "VERSION_NAME":
if propPair[1] != newVersion {
log.Info("updated version from ", propPair[1], " to ", newVersion)
versionUpdated = true
propPair[1] = newVersion
log.Info("updated version to ", newVersion)
}
case "GO_VERSION":
if propPair[1] != runtime.Version() {
log.Info("updated Go version from ", propPair[1], " to ", runtime.Version())
goVersionUpdated = true
propPair[1] = runtime.Version()
log.Info("updated Go version to ", runtime.Version())
}
}
}
if !(versionUpdated || goVersionUpdated) {
log.Info("version not changed")
return
} else if flagRunInCI {
log.Fatal("version changed, commit changes first.")
}
for _, propPair := range propsList {
switch propPair[0] {

View File

@@ -60,7 +60,10 @@ func init() {
generated.WriteString(record[nameIndex])
generated.WriteString("\n")
generated.WriteString(" mozillaIncluded.AppendCertsFromPEM([]byte(`")
generated.WriteString(record[certIndex])
cert := record[certIndex]
// Remove single quotes
cert = cert[1 : len(cert)-1]
generated.WriteString(cert)
generated.WriteString("`))\n")
}
generated.WriteString("}\n")

File diff suppressed because it is too large Load Diff

View File

@@ -22,6 +22,7 @@ type Options struct {
RemoteIsDomain bool
DirectResolver bool
ResolverOnDetour bool
NewDialer bool
}
// TODO: merge with NewWithOptions
@@ -100,6 +101,8 @@ func NewWithOptions(options Options) (N.Dialer, error) {
}
dnsQueryOptions.Transport = transport
resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay)
} else if options.NewDialer {
return nil, E.New("missing domain resolver for domain server address")
} else {
deprecated.Report(options.Context, deprecated.OptionMissingDomainResolver)
}

View File

@@ -18,6 +18,5 @@ func HTTPHost(_ context.Context, metadata *adapter.InboundContext, reader io.Rea
}
metadata.Protocol = C.ProtocolHTTP
metadata.Domain = M.ParseSocksaddr(request.Host).AddrString()
metadata.HTTPRequest = request
return nil
}

View File

@@ -21,7 +21,6 @@ func TLSClientHello(ctx context.Context, metadata *adapter.InboundContext, reade
if clientHello != nil {
metadata.Protocol = C.ProtocolTLS
metadata.Domain = clientHello.ServerName
metadata.ClientHello = clientHello
return nil
}
return err

View File

@@ -8,10 +8,7 @@ import (
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"net"
"time"
M "github.com/sagernet/sing/common/metadata"
)
func GenerateKeyPair(parent *x509.Certificate, parentKey any, timeFunc func() time.Time, serverName string) (*tls.Certificate, error) {
@@ -38,30 +35,17 @@ func GenerateCertificate(parent *x509.Certificate, parentKey any, timeFunc func(
if err != nil {
return
}
var template *x509.Certificate
if serverAddress := M.ParseAddr(serverName); serverAddress.IsValid() {
template = &x509.Certificate{
SerialNumber: serialNumber,
IPAddresses: []net.IP{serverAddress.AsSlice()},
NotBefore: timeFunc().Add(time.Hour * -1),
NotAfter: expire,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
}
} else {
template = &x509.Certificate{
SerialNumber: serialNumber,
NotBefore: timeFunc().Add(time.Hour * -1),
NotAfter: expire,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
Subject: pkix.Name{
CommonName: serverName,
},
DNSNames: []string{serverName},
}
template := &x509.Certificate{
SerialNumber: serialNumber,
NotBefore: timeFunc().Add(time.Hour * -1),
NotAfter: expire,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
Subject: pkix.Name{
CommonName: serverName,
},
DNSNames: []string{serverName},
}
if parent == nil {
parent = template

View File

@@ -27,6 +27,7 @@ const (
DNSTypePreDefined = "predefined"
DNSTypeFakeIP = "fakeip"
DNSTypeDHCP = "dhcp"
DNSTypeTailscale = "tailscale"
)
const (

View File

@@ -23,6 +23,7 @@ const (
TypeVLESS = "vless"
TypeTUIC = "tuic"
TypeHysteria2 = "hysteria2"
TypeTailscale = "tailscale"
)
const (

View File

@@ -1,7 +0,0 @@
package constant
const (
ScriptTypeSurge = "surge"
ScriptSourceTypeLocal = "local"
ScriptSourceTypeRemote = "remote"
)

View File

@@ -301,7 +301,7 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte
return nil, err
}
if r.dnsReverseMapping != nil && len(message.Question) > 0 && response != nil && len(response.Answer) > 0 {
if transport.Type() != C.DNSTypeFakeIP {
if transport == nil || transport.Type() != C.DNSTypeFakeIP {
for _, answer := range response.Answer {
switch record := answer.(type) {
case *mDNS.A:
@@ -345,7 +345,7 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ
r.logger.DebugContext(ctx, "lookup domain ", domain)
ctx, metadata := adapter.ExtendContext(ctx)
metadata.Destination = M.Socksaddr{}
metadata.Domain = domain
metadata.Domain = FqdnToDomain(domain)
if options.Transport != nil {
transport := options.Transport
if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy {

View File

@@ -36,7 +36,10 @@ func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, opt
}, nil
}
func (t *Transport) Start() error {
func (t *Transport) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateStart {
return nil
}
return t.store.Start()
}

View File

@@ -86,9 +86,11 @@ func (t *Transport) exchangeParallel(ctx context.Context, systemConfig *dnsConfi
results := make(chan queryResult)
startRacer := func(ctx context.Context, fqdn string) {
response, err := t.tryOneName(ctx, systemConfig, fqdn, message)
addresses, _ := dns.MessageToAddresses(response)
if len(addresses) == 0 {
err = E.New(fqdn, ": empty result")
if err == nil {
addresses, _ := dns.MessageToAddresses(response)
if len(addresses) == 0 {
err = E.New(fqdn, ": empty result")
}
}
select {
case results <- queryResult{response, err}:

View File

@@ -144,6 +144,13 @@ func (t *UDPTransport) exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.M
func (t *UDPTransport) open(ctx context.Context) (*dnsConnection, error) {
t.access.Lock()
defer t.access.Unlock()
if t.conn != nil {
select {
case <-t.conn.done:
default:
return t.conn, nil
}
}
conn, err := t.dialer.DialContext(ctx, N.NetworkUDP, t.serverAddr)
if err != nil {
return nil, err
@@ -154,6 +161,7 @@ func (t *UDPTransport) open(ctx context.Context) (*dnsConnection, error) {
callbacks: make(map[uint16]*dnsCallback),
}
go t.recvLoop(dnsConn)
t.conn = dnsConn
return dnsConn, nil
}
@@ -201,8 +209,6 @@ type dnsConnection struct {
}
func (c *dnsConnection) Close(err error) {
c.access.Lock()
defer c.access.Unlock()
c.closeOnce.Do(func() {
close(c.done)
c.err = err

View File

@@ -2,7 +2,33 @@
icon: material/alert-decagram
---
#### 1.12.0-alpha.3
#### 1.12.0-alpha.6
* Add Tailscale endpoint **1**
* Drop support for go1.22 **2**
* Fixes and improvements
**1**:
See [Tailscale](/configuration/endpoint/tailscale/).
**2**:
Due to maintenance difficulties, sing-box 1.12.0 requires at least Go 1.23 to compile.
For Windows 7 users, legacy binaries now continue to compile with Go 1.23 and patches from [MetaCubeX/go](https://github.com/MetaCubeX/go).
### 1.11.3
* Fixes and improvements
_This version overwrites 1.11.2, as incorrect binaries were released due to a bug in the continuous integration process._
#### 1.12.0-alpha.5
* Fixes and improvements
### 1.11.1
* Fixes and improvements
@@ -33,18 +59,21 @@ Compatibility for old formats will be removed in sing-box 1.14.0.
Legacy `outbound` DNS rules are deprecated
and can be replaced by the new `domain_resolver` option.
See [Dial Fields](/configuration/shared/dial/#domain_resolver) and
See [Dial Fields](/configuration/shared/dial/#domain_resolver) and
[Route](/configuration/route/#default_domain_resolver).
For migration, see [Migrate outbound DNS rule items to domain resolver](/migration/#migrate-outbound-dns-rule-items-to-domain-resolver).
For migration,
see [Migrate outbound DNS rule items to domain resolver](/migration/#migrate-outbound-dns-rule-items-to-domain-resolver).
**3**:
The new TLS fragment route options allow you to fragment TLS handshakes to bypass firewalls.
This feature is intended to circumvent simple firewalls based on **plaintext packet matching**, and should not be used to circumvent real censorship.
This feature is intended to circumvent simple firewalls based on **plaintext packet matching**, and should not be used
to circumvent real censorship.
Since it is not designed for performance, it should not be applied to all connections, but only to server names that are known to be blocked.
Since it is not designed for performance, it should not be applied to all connections, but only to server names that are
known to be blocked.
See [Route Action](/configuration/route/rule_action/#tls_fragment).

View File

@@ -11,13 +11,15 @@ icon: material/new-box
```json
{
"dns": {
"servers": {
"type": "fakeip",
"tag": "",
"servers": [
{
"type": "fakeip",
"tag": "",
"inet4_range": "198.18.0.0/15",
"inet6_range": "fc00::/18"
}
"inet4_range": "198.18.0.0/15",
"inet6_range": "fc00::/18"
}
]
}
}
```

View File

@@ -11,20 +11,22 @@ icon: material/new-box
```json
{
"dns": {
"servers": {
"type": "h3",
"tag": "",
"server": "",
"server_port": 443,
"path": "",
"headers": {},
"tls": {},
// Dial Fields
}
"servers": [
{
"type": "h3",
"tag": "",
"server": "",
"server_port": 443,
"path": "",
"headers": {},
"tls": {},
// Dial Fields
}
]
}
}
```

View File

@@ -11,20 +11,22 @@ icon: material/new-box
```json
{
"dns": {
"servers": {
"type": "https",
"tag": "",
"server": "",
"server_port": 443,
"path": "",
"headers": {},
"tls": {},
// Dial Fields
}
"servers": [
{
"type": "https",
"tag": "",
"server": "",
"server_port": 443,
"path": "",
"headers": {},
"tls": {},
// Dial Fields
}
]
}
}
```

View File

@@ -11,17 +11,19 @@ icon: material/new-box
```json
{
"dns": {
"servers": {
"type": "quic",
"tag": "",
"server": "",
"server_port": 853,
"tls": {},
// Dial Fields
}
"servers": [
{
"type": "quic",
"tag": "",
"server": "",
"server_port": 853,
"tls": {},
// Dial Fields
}
]
}
}
```

View File

@@ -11,15 +11,17 @@ icon: material/new-box
```json
{
"dns": {
"servers": {
"type": "tcp",
"tag": "",
"server": "",
"server_port": 53,
// Dial Fields
}
"servers": [
{
"type": "tcp",
"tag": "",
"server": "",
"server_port": 53,
// Dial Fields
}
]
}
}
```

View File

@@ -11,17 +11,19 @@ icon: material/new-box
```json
{
"dns": {
"servers": {
"type": "tls",
"tag": "",
"server": "",
"server_port": 853,
"tls": {},
// Dial Fields
}
"servers": [
{
"type": "tls",
"tag": "",
"server": "",
"server_port": 853,
"tls": {},
// Dial Fields
}
]
}
}
```

View File

@@ -11,15 +11,17 @@ icon: material/new-box
```json
{
"dns": {
"servers": {
"type": "udp",
"tag": "",
"server": "",
"server_port": 53,
// Dial Fields
}
"servers": [
{
"type": "udp",
"tag": "",
"server": "",
"server_port": 53,
// Dial Fields
}
]
}
}
```

View File

@@ -0,0 +1,90 @@
---
icon: material/new-box
---
!!! question "Since sing-box 1.12.0"
### Structure
```json
{
"type": "tailscale",
"tag": "ts-ep",
"state_directory": "",
"auth_key": "",
"control_url": "",
"ephemeral": false,
"hostname": "",
"exit_node": "",
"exit_node_allow_lan_access": false,
"advertise_routes": [],
"udp_timeout": "5m",
... // Dial Fields
}
```
### Fields
#### state_directory
The directory where the Tailscale state is stored.
`tailscale` is used by default.
Example: `$HOME/.tailscale`
#### auth_key
The auth key to create the node. If the node is already created (from state previously stored), then this field is not
used.
#### control_url
The coordination server URL.
`https://controlplane.tailscale.com` is used by default.
#### ephemeral
Indicates whether the instance should register as an Ephemeral node (https://tailscale.com/s/ephemeral-nodes).
#### hostname
The hostname of the node.
System hostname is used by default.
Example: `localhost`
#### exit_node
The exit node name or IP address to use.
#### exit_node_allow_lan_access
!!! note
When the exit node does not have a corresponding advertised route, private traffics cannot be routed to the exit node even if `exit_node_allow_lan_access is` set.
Indicates whether locally accessible subnets should be routed directly or via the exit node.
#### advertise_routes
CIDR prefixes to advertise into the Tailscale network as reachable through the current node.
Example: `["192.168.1.1/24"]`
#### udp_timeout
UDP NAT expiration time.
`5m` will be used by default.
### Dial Fields
!!! note
Dial Fields in Tailscale endpoints only control how it connects to the control plane and have nothing to do with actual connections.
See [Dial Fields](/configuration/shared/dial/) for details.

View File

@@ -8,6 +8,7 @@ sing-box 使用 JSON 作为配置文件格式。
{
"log": {},
"dns": {},
"ntp": {},
"endpoints": [],
"inbounds": [],
"outbounds": [],
@@ -22,6 +23,7 @@ sing-box 使用 JSON 作为配置文件格式。
|----------------|------------------------|
| `log` | [日志](./log/) |
| `dns` | [DNS](./dns/) |
| `ntp` | [NTP](./ntp/) |
| `endpoints` | [端点](./endpoint/) |
| `inbounds` | [入站](./inbound/) |
| `outbounds` | [出站](./outbound/) |

View File

@@ -8,7 +8,7 @@ icon: material/delete-clock
### Structure
```json F
```json
{
"type": "block",
"tag": "block"

View File

@@ -70,7 +70,7 @@ Tag of rule-set.
==Required==
List of [Headless Rule](../headless-rule/).
List of [Headless Rule](./headless-rule/).
### Local or Remote Fields

View File

@@ -70,7 +70,7 @@
==必填==
一组 [无头规则](../headless-rule/).
一组 [无头规则](./headless-rule/).
### 本地或远程字段

View File

@@ -60,5 +60,6 @@ go build -tags "tag_a tag_b" ./cmd/sing-box
| `with_v2ray_api` | :material-close: | Build with V2Ray API support, see [Experimental](/configuration/experimental#v2ray-api-fields). |
| `with_gvisor` | :material-check: | Build with gVisor support, see [Tun inbound](/configuration/inbound/tun#stack) and [WireGuard outbound](/configuration/outbound/wireguard#system_interface). |
| `with_embedded_tor` (CGO required) | :material-close: | Build with embedded Tor support, see [Tor outbound](/configuration/outbound/tor/). |
| `with_tailscale` | :material-check: | Build with Tailscale support, see [Tailscale endpoint](/configuration/endpoint/tailscale) |
It is not recommended to change the default build tag list unless you really know what you are adding.

View File

@@ -60,5 +60,6 @@ go build -tags "tag_a tag_b" ./cmd/sing-box
| `with_v2ray_api` | :material-close: | Build with V2Ray API support, see [Experimental](/configuration/experimental#v2ray-api-fields). |
| `with_gvisor` | :material-check: | Build with gVisor support, see [Tun inbound](/configuration/inbound/tun#stack) and [WireGuard outbound](/configuration/outbound/wireguard#system_interface). |
| `with_embedded_tor` (CGO required) | :material-close: | Build with embedded Tor support, see [Tor outbound](/configuration/outbound/tor/). |
| `with_tailscale` | :material-check: | Build with Tailscale support, see [Tailscale endpoint](/configuration/endpoint/tailscale) |
除非您确实知道您正在启用什么,否则不建议更改默认构建标签列表。

View File

@@ -692,7 +692,7 @@ Inbound fields are deprecated and can be replaced by rule actions.
!!! info "References"
[Listen Fields](/configuration/inbound/listen/) /
[Listen Fields](/configuration/shared/listen/) /
[Rule](/configuration/route/rule/) /
[Rule Action](/configuration/route/rule_action/) /
[DNS Rule](/configuration/dns/rule/) /

View File

@@ -19,12 +19,10 @@ import (
)
var (
bucketSelected = []byte("selected")
bucketExpand = []byte("group_expand")
bucketMode = []byte("clash_mode")
bucketRuleSet = []byte("rule_set")
bucketScript = []byte("script")
bucketSgPersistentStore = []byte("sg_persistent_store")
bucketSelected = []byte("selected")
bucketExpand = []byte("group_expand")
bucketMode = []byte("clash_mode")
bucketRuleSet = []byte("rule_set")
bucketNameList = []string{
string(bucketSelected),
@@ -318,70 +316,3 @@ func (c *CacheFile) SaveRuleSet(tag string, set *adapter.SavedBinary) error {
return bucket.Put([]byte(tag), setBinary)
})
}
func (c *CacheFile) LoadScript(tag string) *adapter.SavedBinary {
var savedSet adapter.SavedBinary
err := c.DB.View(func(t *bbolt.Tx) error {
bucket := c.bucket(t, bucketScript)
if bucket == nil {
return os.ErrNotExist
}
scriptBinary := bucket.Get([]byte(tag))
if len(scriptBinary) == 0 {
return os.ErrInvalid
}
return savedSet.UnmarshalBinary(scriptBinary)
})
if err != nil {
return nil
}
return &savedSet
}
func (c *CacheFile) SaveScript(tag string, set *adapter.SavedBinary) error {
return c.DB.Batch(func(t *bbolt.Tx) error {
bucket, err := c.createBucket(t, bucketScript)
if err != nil {
return err
}
scriptBinary, err := set.MarshalBinary()
if err != nil {
return err
}
return bucket.Put([]byte(tag), scriptBinary)
})
}
func (c *CacheFile) SurgePersistentStoreRead(key string) string {
var value string
_ = c.DB.View(func(t *bbolt.Tx) error {
bucket := c.bucket(t, bucketSgPersistentStore)
if bucket == nil {
return nil
}
valueBinary := bucket.Get([]byte(key))
if len(valueBinary) > 0 {
value = string(valueBinary)
}
return nil
})
return value
}
func (c *CacheFile) SurgePersistentStoreWrite(key string, value string) error {
return c.DB.Batch(func(t *bbolt.Tx) error {
if value != "" {
bucket, err := c.createBucket(t, bucketSgPersistentStore)
if err != nil {
return err
}
return bucket.Put([]byte(key), []byte(value))
} else {
bucket := c.bucket(t, bucketSgPersistentStore)
if bucket == nil {
return nil
}
return bucket.Delete([]byte(key))
}
})
}

View File

@@ -1,84 +0,0 @@
package clashapi
import (
"context"
"net/http"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/service"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
"github.com/gofrs/uuid/v5"
"howett.net/plist"
)
func mitmRouter(ctx context.Context) http.Handler {
r := chi.NewRouter()
r.Get("/mobileconfig", getMobileConfig(ctx))
r.Get("/certificate", getCertificate(ctx))
return r
}
func getMobileConfig(ctx context.Context) http.HandlerFunc {
return func(writer http.ResponseWriter, request *http.Request) {
engine := service.FromContext[adapter.MITMEngine](ctx)
if engine == nil {
http.NotFound(writer, request)
render.PlainText(writer, request, "MITM not enabled")
return
}
certificate := engine.ExportCertificate()
if certificate == nil {
http.NotFound(writer, request)
render.PlainText(writer, request, "Certificate not configured")
return
}
writer.Header().Set("Content-Type", "application/x-apple-aspen-config")
uuidGen := common.Must1(uuid.NewV4()).String()
mobileConfig := map[string]interface{}{
"PayloadContent": []interface{}{
map[string]interface{}{
"PayloadCertificateFileName": "Certificates.cer",
"PayloadContent": certificate.Raw,
"PayloadDescription": "Adds a root certificate",
"PayloadDisplayName": certificate.Subject.CommonName,
"PayloadIdentifier": "com.apple.security.root." + uuidGen,
"PayloadType": "com.apple.security.root",
"PayloadUUID": uuidGen,
"PayloadVersion": 1,
},
},
"PayloadDisplayName": certificate.Subject.CommonName,
"PayloadIdentifier": "io.nekohasekai.sfa.ca.profile." + uuidGen,
"PayloadRemovalDisallowed": false,
"PayloadType": "Configuration",
"PayloadUUID": uuidGen,
"PayloadVersion": 1,
}
encoder := plist.NewEncoder(writer)
encoder.Indent("\t")
encoder.Encode(mobileConfig)
}
}
func getCertificate(ctx context.Context) http.HandlerFunc {
return func(writer http.ResponseWriter, request *http.Request) {
engine := service.FromContext[adapter.MITMEngine](ctx)
if engine == nil {
http.NotFound(writer, request)
render.PlainText(writer, request, "MITM not enabled")
return
}
certificate := engine.ExportCertificate()
if certificate == nil {
http.NotFound(writer, request)
render.PlainText(writer, request, "Certificate not configured")
return
}
writer.Header().Set("Content-Type", "application/x-x509-ca-cert")
writer.Header().Set("Content-Disposition", "attachment; filename=Certificate.crt")
writer.Write(certificate.Raw)
}
}

View File

@@ -124,7 +124,6 @@ func NewServer(ctx context.Context, logFactory log.ObservableFactory, options op
r.Mount("/profile", profileRouter())
r.Mount("/cache", cacheRouter(ctx))
r.Mount("/dns", dnsRouter(s.dnsRouter))
r.Mount("/mitm", mitmRouter(ctx))
s.setupMetaAPI(r)
})

View File

@@ -32,9 +32,4 @@ type Notification struct {
Subtitle string
Body string
OpenURL string
Clipboard string
MediaURL string
MediaData []byte
MediaType string
Timeout int
}

96
go.mod
View File

@@ -1,23 +1,23 @@
module github.com/sagernet/sing-box
go 1.20
go 1.23.1
toolchain go1.24.0
require (
github.com/adhocore/gronx v1.19.5
github.com/caddyserver/certmagic v0.20.0
github.com/cloudflare/circl v1.3.7
github.com/cretz/bine v0.2.0
github.com/dop251/goja v0.0.0-20250125213203-5ef83b82af17
github.com/go-chi/chi/v5 v5.1.0
github.com/go-chi/chi/v5 v5.2.1
github.com/go-chi/render v1.0.3
github.com/gofrs/uuid/v5 v5.3.0
github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2
github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905
github.com/libdns/alidns v1.0.3
github.com/libdns/cloudflare v0.1.1
github.com/logrusorgru/aurora v2.0.3+incompatible
github.com/metacubex/tfo-go v0.0.0-20241006021335-daedaf0ca7aa
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422
github.com/mholt/acmez v1.2.0
github.com/miekg/dns v1.1.62
github.com/miekg/dns v1.1.63
github.com/oschwald/maxminddb-golang v1.12.0
github.com/sagernet/asc-go v0.0.0-20241217030726-d563060fe4e1
github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a
@@ -28,27 +28,28 @@ require (
github.com/sagernet/gvisor v0.0.0-20241123041152-536d05261cff
github.com/sagernet/quic-go v0.49.0-beta.1
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691
github.com/sagernet/sing v0.6.0-beta.12.0.20250130112616-23af22fe01ff
github.com/sagernet/sing-mux v0.3.0-alpha.1
github.com/sagernet/sing-quic v0.4.0-beta.4
github.com/sagernet/sing v0.6.2-0.20250210105917-3464ed3babc0
github.com/sagernet/sing-mux v0.3.1
github.com/sagernet/sing-quic v0.4.0
github.com/sagernet/sing-shadowsocks v0.2.7
github.com/sagernet/sing-shadowsocks2 v0.2.0
github.com/sagernet/sing-shadowtls v0.2.0-alpha.2
github.com/sagernet/sing-tun v0.6.0-beta.8
github.com/sagernet/sing-vmess v0.2.0-beta.2
github.com/sagernet/sing-shadowtls v0.2.0
github.com/sagernet/sing-tun v0.6.1
github.com/sagernet/sing-vmess v0.2.0
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7
github.com/sagernet/tailscale v1.79.0-mod.1
github.com/sagernet/utls v1.6.7
github.com/sagernet/wireguard-go v0.0.1-beta.5
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854
github.com/spf13/cobra v1.8.1
github.com/stretchr/testify v1.9.0
github.com/stretchr/testify v1.10.0
go.uber.org/zap v1.27.0
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
golang.org/x/crypto v0.32.0
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
golang.org/x/mod v0.20.0
golang.org/x/net v0.34.0
golang.org/x/sys v0.29.0
golang.org/x/sys v0.30.0
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
google.golang.org/grpc v1.63.2
google.golang.org/protobuf v1.33.0
@@ -58,48 +59,87 @@ require (
//replace github.com/sagernet/sing => ../sing
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/ajg/form v1.5.1 // indirect
github.com/andybalholm/brotli v1.0.6 // indirect
github.com/akutz/memconn v0.1.0 // indirect
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/bits-and-blooms/bitset v1.13.0 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/coder/websocket v1.8.12 // indirect
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect
github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1 // indirect
github.com/dlclark/regexp2 v1.11.4 // indirect
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/fxamacker/cbor/v2 v2.6.0 // indirect
github.com/gaissmai/bart v0.11.1 // indirect
github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 // indirect
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 // indirect
github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/csrf v1.7.2 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/hashicorp/yamux v0.1.2 // indirect
github.com/hdevalence/ed25519consensus v0.2.0 // indirect
github.com/illarion/gonotify/v2 v2.0.3 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/josharian/native v1.1.0 // indirect
github.com/klauspost/compress v1.17.4 // indirect
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86 // indirect
github.com/jsimonetti/rtnetlink v1.4.0 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a // indirect
github.com/libdns/libdns v0.2.2 // indirect
github.com/mdlayher/genetlink v1.3.2 // indirect
github.com/mdlayher/netlink v1.7.2 // indirect
github.com/mdlayher/socket v0.4.1 // indirect
github.com/onsi/ginkgo/v2 v2.9.7 // indirect
github.com/pierrec/lz4/v4 v4.1.14 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/mdlayher/sdnotify v1.0.0 // indirect
github.com/mdlayher/socket v0.5.0 // indirect
github.com/mitchellh/go-ps v1.0.0 // indirect
github.com/onsi/ginkgo/v2 v2.17.2 // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus-community/pro-bing v0.4.0 // indirect
github.com/quic-go/qpack v0.4.0 // indirect
github.com/quic-go/qtls-go1-20 v0.4.1 // indirect
github.com/safchain/ethtool v0.3.0 // indirect
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect
github.com/sagernet/nftables v0.3.0-beta.4 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect
github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e // indirect
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect
github.com/tailscale/golang-x-crypto v0.0.0-20240604161659-3fde5e568aa4 // indirect
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 // indirect
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a // indirect
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 // indirect
github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4 // indirect
github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1 // indirect
github.com/tcnksm/go-httpstat v0.2.0 // indirect
github.com/u-root/uio v0.0.0-20240118234441-a3c409a6018e // indirect
github.com/vishvananda/netns v0.0.4 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/zeebo/blake3 v0.2.3 // indirect
go.uber.org/multierr v1.11.0 // indirect
go4.org/mem v0.0.0-20220726221520-4f986261bf13 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/term v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/time v0.7.0 // indirect
golang.org/x/tools v0.24.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/blake3 v1.3.0 // indirect
)
//replace github.com/sagernet/sing => ../sing

211
go.sum
View File

@@ -1,48 +1,69 @@
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
github.com/adhocore/gronx v1.19.5 h1:cwIG4nT1v9DvadxtHBe6MzE+FZ1JDvAUC45U2fl4eSQ=
github.com/adhocore/gronx v1.19.5/go.mod h1:7oUY1WAU8rEJWmAxXR2DN0JaO4gi9khSgKjiRypqteg=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A=
github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw=
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE=
github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/caddyserver/certmagic v0.20.0 h1:bTw7LcEZAh9ucYCRXyCpIrSAGplplI0vGYJ4BpCQ/Fc=
github.com/caddyserver/certmagic v0.20.0/go.mod h1:N4sXgpICQUskEWpj7zVzvWD41p3NYacrNoZYiRM2jTg=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cilium/ebpf v0.15.0 h1:7NxJhNiBT3NG8pZJ3c+yfrVdHY8ScgKD27sScgjLMMk=
github.com/cilium/ebpf v0.15.0/go.mod h1:DHp1WyrLeiBh19Cf/tfiSMhqheEiK8fXFZ4No0P1Hso=
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 h1:8h5+bWd7R6AYUslN6c6iuZWTKsKxUFDlpnmilO6R2n0=
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cretz/bine v0.2.0 h1:8GiDRGlTgz+o8H9DSnsl+5MeBK4HsExxgl6WgzOCuZo=
github.com/cretz/bine v0.2.0/go.mod h1:WU4o9QR9wWp8AVKtTM1XD5vUHkEqnf2vVSo6dBqbetI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa h1:h8TfIT1xc8FWbwwpmHn1J5i43Y0uZP97GqasGCzSRJk=
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa/go.mod h1:Nx87SkVqTKd8UtT+xu7sM/l+LgXs6c0aHrlKusR+2EQ=
github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1 h1:CaO/zOnF8VvUfEbhRatPcwKVWamvbYd8tQGRWacE9kU=
github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1/go.mod h1:+hnT3ywWDTAFrW5aE+u2Sa/wT555ZqwoCS+pk3p6ry4=
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dop251/goja v0.0.0-20250125213203-5ef83b82af17 h1:spJaibPy2sZNwo6Q0HjBVufq7hBUj5jNFOKRoogCBow=
github.com/dop251/goja v0.0.0-20250125213203-5ef83b82af17/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4=
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yezR0y7jJ5pceLHthLaYf4bA5T14B6q39S4q2Q=
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/fxamacker/cbor/v2 v2.6.0 h1:sU6J2usfADwWlYDAFhZBQ6TnLFBHxgesMrQfQgk1tWA=
github.com/fxamacker/cbor/v2 v2.6.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/gaissmai/bart v0.11.1 h1:5Uv5XwsaFBRo4E5VBcb9TzY8B7zxFf+U7isDxqOrRfc=
github.com/gaissmai/bart v0.11.1/go.mod h1:KHeYECXQiBjTzQz/om2tqn3sZF1J7hw9m6z41ftj3fg=
github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I=
github.com/github/fakeca v0.1.0/go.mod h1:+bormgoGMMuamOscx7N91aOuUST7wdaJ2rNjeohylyo=
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0 h1:ymLjT4f35nQbASLnvxEde4XOBL+Sn7rFuV+FOJqkljg=
github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0/go.mod h1:6daplAwHHGbUGib4990V3Il26O0OC4aRyvewaaAihaA=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-sourcemap/sourcemap v2.1.4+incompatible h1:a+iTbH5auLKxaNwQFg0B+TCYl6lbukKPc7b5x0n1s6Q=
github.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 h1:sQspH8M4niEijh3PFscJRLDnkL547IeP7kpPe3uUhEg=
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466/go.mod h1:ZiQxhyQ+bbbfxUKVvjfO498oPYvtYhZzycal3G/NHmU=
github.com/gofrs/uuid/v5 v5.3.0 h1:m0mUMr+oVYUdxpMLgSYCZiXe7PuVPnI94+OMeVBNedk=
github.com/gofrs/uuid/v5 v5.3.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
@@ -50,23 +71,41 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQuYXQ+6EN5stWmeY/AZqtM8xk9k=
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 h1:wG8RYIyctLhdFk6Vl1yPGtSRtwGpVkWyZww1OCil2MI=
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4=
github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 h1:k7nVchz72niMH6YLQNvHSdIE7iqsQxK1P41mySCvssg=
github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/csrf v1.7.2 h1:oTUjx0vyf2T+wkrx09Trsev1TE+/EbDAeHtSTbtC2eI=
github.com/gorilla/csrf v1.7.2/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=
github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU=
github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo=
github.com/illarion/gonotify/v2 v2.0.3 h1:B6+SKPo/0Sw8cRJh1aLzNEeNVFfzE3c6N+o+vyxM+9A=
github.com/illarion/gonotify/v2 v2.0.3/go.mod h1:38oIJTgFqupkEydkkClkbL6i5lXV/bxdH9do5TALPEE=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 h1:9K06NfxkBh25x56yVhWWlKFE8YpicaSfHwoV8SFbueA=
github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2/go.mod h1:3A9PQ1cunSDF/1rbTq99Ts4pVnycWg+vlPkfeD2NLFI=
github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905 h1:q3OEI9RaN/wwcx+qgGo6ZaoJkCiDYe/gjDLfq7lQQF4=
github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905/go.mod h1:VvGYjkZoJyKqlmT1yzakUs4mfKMNB0XdODP0+rdml6k=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86 h1:elKwZS1OcdQ0WwEDBeqxKwb7WB62QX8bvZ/FJnVXIfk=
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86/go.mod h1:aFAMtuldEgx/4q7iSGazk22+IcgvtiC+HIimFO9XlS8=
github.com/jsimonetti/rtnetlink v1.4.0 h1:Z1BF0fRgcETPEa0Kt0MRk3yV5+kF1FWTni6KUFKrq2I=
github.com/jsimonetti/rtnetlink v1.4.0/go.mod h1:5W1jDvWdnthFJ7fxYX1GMK07BUpI4oskfOqvPteYS6E=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a h1:+RR6SqnTkDLWyICxS1xpjCi/3dhyV+TgZwA6Ww3KncQ=
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a/go.mod h1:YTtCCM3ryyfiu4F7t8HQ1mxvp1UBdWM2r6Xa+nGWvDk=
github.com/libdns/alidns v1.0.3 h1:LFHuGnbseq5+HCeGa1aW8awyX/4M2psB9962fdD2+yQ=
github.com/libdns/alidns v1.0.3/go.mod h1:e18uAG6GanfRhcJj6/tps2rCMzQJaYVcGKT+ELjdjGE=
github.com/libdns/cloudflare v0.1.1 h1:FVPfWwP8zZCqj268LZjmkDleXlHPlFU9KC4OJ3yn054=
@@ -76,30 +115,45 @@ github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s=
github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8=
github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
github.com/metacubex/tfo-go v0.0.0-20241006021335-daedaf0ca7aa h1:9mcjV+RGZVC3reJBNDjjNPyS8PmFG97zq56X7WNaFO4=
github.com/metacubex/tfo-go v0.0.0-20241006021335-daedaf0ca7aa/go.mod h1:4tLB5c8U0CxpkFM+AJJB77jEaVDbLH5XQvy42vAGsWw=
github.com/mdlayher/sdnotify v1.0.0 h1:Ma9XeLVN/l0qpyx1tNeMSeTjCPH6NtuD6/N9XdTlQ3c=
github.com/mdlayher/sdnotify v1.0.0/go.mod h1:HQUmpM4XgYkhDLtd+Uad8ZFK1T9D5+pNxnXQjCeJlGE=
github.com/mdlayher/socket v0.5.0 h1:ilICZmJcQz70vrWVes1MFera4jGiWNocSkykwwoy3XI=
github.com/mdlayher/socket v0.5.0/go.mod h1:WkcBFfvyG8QENs5+hfQPl1X6Jpd2yeLIYgrGFmJiJxI=
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 h1:zGeQt3UyNydIVrMRB97AA5WsYEau/TyCnRtTf1yUmJY=
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw=
github.com/mholt/acmez v1.2.0 h1:1hhLxSgY5FvH5HCnGUuwbKY2VQVo8IU7rxXKSnZ7F30=
github.com/mholt/acmez v1.2.0/go.mod h1:VT9YwH1xgNX1kmYY89gY8xPJC84BFAisjo8Egigt4kE=
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
github.com/onsi/ginkgo/v2 v2.9.7 h1:06xGQy5www2oN160RtEZoTvnP2sPhEfePYmCDc2szss=
github.com/onsi/ginkgo/v2 v2.9.7/go.mod h1:cxrmXWykAwTwhQsJOPfdIDiJ+l2RYq7U8hFU+M/1uw0=
github.com/onsi/gomega v1.27.7 h1:fVih9JD6ogIiHUN6ePK7HJidyEDpWGVB5mzM7cWNXoU=
github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=
github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs=
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/onsi/ginkgo/v2 v2.17.2 h1:7eMhcy3GimbsA3hEnVKdw/PQM9XN9krpKVXsZdph0/g=
github.com/onsi/ginkgo/v2 v2.17.2/go.mod h1:nP2DPOQoNsQmsVyv5rDA8JkXQoCs6goXIvr/PRJ1eCc=
github.com/onsi/gomega v1.33.0 h1:snPCflnZrpMsy94p4lXVEkHo12lmPnc3vY5XBbreexE=
github.com/onsi/gomega v1.33.0/go.mod h1:+925n5YtiFsLzzafLUHzVMBpvvRAzrydIBiSIxjX3wY=
github.com/oschwald/maxminddb-golang v1.12.0 h1:9FnTOD0YOhP7DGxGsq4glzpGy5+w7pq50AS6wALUMYs=
github.com/oschwald/maxminddb-golang v1.12.0/go.mod h1:q0Nob5lTCqyQ8WT6FYgS1L7PXKVVbgiymefNwIjPzgY=
github.com/pierrec/lz4/v4 v4.1.14 h1:+fL8AQEZtz/ijeNnpduH0bROTu0O3NZAlPjQxGn8LwE=
github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus-community/pro-bing v0.4.0 h1:YMbv+i08gQz97OZZBwLyvmmQEEzyfyrrjEaAchdy3R4=
github.com/prometheus-community/pro-bing v0.4.0/go.mod h1:b7wRYZtCcPmt4Sz319BykUU241rWLe1VFXyiyWK/dH4=
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs=
github.com/quic-go/qtls-go1-20 v0.4.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0=
github.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs=
github.com/sagernet/asc-go v0.0.0-20241217030726-d563060fe4e1 h1:qi+ijeREa0yfAaO+NOcZ81gv4uzOfALUIdhkiIFvmG4=
github.com/sagernet/asc-go v0.0.0-20241217030726-d563060fe4e1/go.mod h1:JULDuzTMn2gyZFcjpTVZP4/UuwAdbHJ0bum2RdjXojU=
github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a h1:+NkI2670SQpQWvkkD2QgdTuzQG263YZ+2emfpeyGqW0=
@@ -123,24 +177,26 @@ github.com/sagernet/quic-go v0.49.0-beta.1/go.mod h1:uesWD1Ihrldq1M3XtjuEvIUqi8W
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byLGkEnIYp6grlXfo1QYUfiYFGjewIdc=
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU=
github.com/sagernet/sing v0.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo=
github.com/sagernet/sing v0.6.0-beta.12.0.20250130112616-23af22fe01ff h1:aZeWJw/NkI3/NoXbNHzv2435nWYTV+ZDNDWXTnN9FjQ=
github.com/sagernet/sing v0.6.0-beta.12.0.20250130112616-23af22fe01ff/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing-mux v0.3.0-alpha.1 h1:IgNX5bJBpL41gGbp05pdDOvh/b5eUQ6cv9240+Ngipg=
github.com/sagernet/sing-mux v0.3.0-alpha.1/go.mod h1:FTcImmdfW38Lz7b+HQ+mxxOth1lz4ao8uEnz+MwIJQE=
github.com/sagernet/sing-quic v0.4.0-beta.4 h1:kKiMLGaxvVLDCSvCMYo4PtWd1xU6FTL7xvUAQfXO09g=
github.com/sagernet/sing-quic v0.4.0-beta.4/go.mod h1:1UNObFodd8CnS3aCT53x9cigjPSCl3P//8dfBMCwBDM=
github.com/sagernet/sing v0.6.2-0.20250210105917-3464ed3babc0 h1:8gTBdpb2JeNq4oAb8AHCisUI+mZlrpD0qMRrsb9EnAo=
github.com/sagernet/sing v0.6.2-0.20250210105917-3464ed3babc0/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing-mux v0.3.1 h1:kvCc8HyGAskDHDQ0yQvoTi/7J4cZPB/VJMsAM3MmdQI=
github.com/sagernet/sing-mux v0.3.1/go.mod h1:Mkdz8LnDstthz0HWuA/5foncnDIdcNN5KZ6AdJX+x78=
github.com/sagernet/sing-quic v0.4.0 h1:E4geazHk/UrJTXMlT+CBCKmn8V86RhtNeczWtfeoEFc=
github.com/sagernet/sing-quic v0.4.0/go.mod h1:c+CytOEyeN20KCTFIP8YQUkNDVFLSzjrEPqP7Hlnxys=
github.com/sagernet/sing-shadowsocks v0.2.7 h1:zaopR1tbHEw5Nk6FAkM05wCslV6ahVegEZaKMv9ipx8=
github.com/sagernet/sing-shadowsocks v0.2.7/go.mod h1:0rIKJZBR65Qi0zwdKezt4s57y/Tl1ofkaq6NlkzVuyE=
github.com/sagernet/sing-shadowsocks2 v0.2.0 h1:wpZNs6wKnR7mh1wV9OHwOyUr21VkS3wKFHi+8XwgADg=
github.com/sagernet/sing-shadowsocks2 v0.2.0/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ=
github.com/sagernet/sing-shadowtls v0.2.0-alpha.2 h1:RPrpgAdkP5td0vLfS5ldvYosFjSsZtRPxiyLV6jyKg0=
github.com/sagernet/sing-shadowtls v0.2.0-alpha.2/go.mod h1:0j5XlzKxaWRIEjc1uiSKmVoWb0k+L9QgZVb876+thZA=
github.com/sagernet/sing-tun v0.6.0-beta.8 h1:GFNt/w8r1v30zC/hfCytk8C9+N/f1DfvosFXJkyJlrw=
github.com/sagernet/sing-tun v0.6.0-beta.8/go.mod h1:fisFCbC4Vfb6HqQNcwPJi2CDK2bf0Xapyz3j3t4cnHE=
github.com/sagernet/sing-vmess v0.2.0-beta.2 h1:obAkAL35X7ql4RnGzDg4dBYIRpGXRKqcN4LyLZpZGSs=
github.com/sagernet/sing-vmess v0.2.0-beta.2/go.mod h1:HGhf9XUdeE2iOWrX0hQNFgXPbKyGlzpeYFyX0c/pykk=
github.com/sagernet/sing-shadowtls v0.2.0 h1:cLKe4OAOFwuhmAIuPLj//CIL7Q9js+pIDardhJ+/osk=
github.com/sagernet/sing-shadowtls v0.2.0/go.mod h1:agU+Fw5X+xnWVyRHyFthoZCX3MfWKCFPm4JUf+1oaxo=
github.com/sagernet/sing-tun v0.6.1 h1:4l0+gnEKcGjlWfUVTD+W0BRApqIny/lU2ZliurE+VMo=
github.com/sagernet/sing-tun v0.6.1/go.mod h1:fisFCbC4Vfb6HqQNcwPJi2CDK2bf0Xapyz3j3t4cnHE=
github.com/sagernet/sing-vmess v0.2.0 h1:pCMGUXN2k7RpikQV65/rtXtDHzb190foTfF9IGTMZrI=
github.com/sagernet/sing-vmess v0.2.0/go.mod h1:jDAZ0A0St1zVRkyvhAPRySOFfhC+4SQtO5VYyeFotgA=
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ=
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7/go.mod h1:FP9X2xjT/Az1EsG/orYYoC+5MojWnuI7hrffz8fGwwo=
github.com/sagernet/tailscale v1.79.0-mod.1 h1:wIuAH7VqBYJNk0h2+bTyk4F0OlSqHvyLDCBrD3i+XNI=
github.com/sagernet/tailscale v1.79.0-mod.1/go.mod h1:RKY5WjYLj3JJ7VO/8ZCw8eAFa4+kWU6A1Ftdk84uB14=
github.com/sagernet/utls v1.6.7 h1:Ep3+aJ8FUGGta+II2IEVNUc3EDhaRCZINWkj/LloIA8=
github.com/sagernet/utls v1.6.7/go.mod h1:Uua1TKO/FFuAhLr9rkaVnnrTmmiItzDjv1BUb2+ERwM=
github.com/sagernet/wireguard-go v0.0.1-beta.5 h1:aBEsxJUMEONwOZqKPIkuAcv4zJV5p6XlzEN04CF0FXc=
@@ -152,14 +208,36 @@ github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3k
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 h1:tHNk7XK9GkmKUR6Gh8gVBKXc2MVSZ4G/NnWLtzw4gNA=
github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e h1:PtWT87weP5LWHEY//SWsYkSO3RWRZo4OSWagh3YD2vQ=
github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e/go.mod h1:XrBNfAFN+pwoWuksbFS9Ccxnopa15zJGgXRFN90l3K4=
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4=
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg=
github.com/tailscale/golang-x-crypto v0.0.0-20240604161659-3fde5e568aa4 h1:rXZGgEa+k2vJM8xT0PoSKfVXwFGPQ3z3CJfmnHJkZZw=
github.com/tailscale/golang-x-crypto v0.0.0-20240604161659-3fde5e568aa4/go.mod h1:ikbF+YT089eInTp9f2vmvy4+ZVnW5hzX1q2WknxSprQ=
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 h1:4chzWmimtJPxRs2O36yuGRW3f9SYV+bMTTvMBI0EKio=
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8=
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a h1:SJy1Pu0eH1C29XwJucQo73FrleVK6t4kYz4NVhp34Yw=
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a/go.mod h1:DFSS3NAGHthKo1gTlmEcSBiZrRJXi28rLNd/1udP1c8=
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 h1:uFsXVBE9Qr4ZoF094vE6iYTLDl0qCiKzYXlL6UeWObU=
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0=
github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4 h1:Gz0rz40FvFVLTBk/K8UNAenb36EbDSnh+q7Z9ldcC8w=
github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4/go.mod h1:phI29ccmHQBc+wvroosENp1IF9195449VDnFDhJ4rJU=
github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1 h1:tdUdyPqJ0C97SJfjB9tW6EylTtreyee9C44de+UBG0g=
github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ=
github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA=
github.com/tc-hib/winres v0.2.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk=
github.com/tcnksm/go-httpstat v0.2.0 h1:rP7T5e5U2HfmOBmZzGgGZjBQ5/GluWUylujl0tJ04I0=
github.com/tcnksm/go-httpstat v0.2.0/go.mod h1:s3JVJFtQxtBEBC9dwcdTTXS9xFnM3SXAZwPG41aurT8=
github.com/u-root/uio v0.0.0-20240118234441-a3c409a6018e h1:BA9O3BmlTmpjbvajAwzWx4Wo2TRVdpPXZEeemGQcajw=
github.com/u-root/uio v0.0.0-20240118234441-a3c409a6018e/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264=
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg=
@@ -167,10 +245,13 @@ github.com/zeebo/blake3 v0.2.3/go.mod h1:mjJjZpnsyIVtVgTOSpJ9vmRE4wgDeyt2HU3qXvv
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go4.org/mem v0.0.0-20220726221520-4f986261bf13 h1:CbZeCBZ0aZj8EfVgnqQcYZgf0lpZ3H9rmp5nkDTAst8=
go4.org/mem v0.0.0-20220726221520-4f986261bf13/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
@@ -178,25 +259,33 @@ golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.1-0.20230131160137-e7d7f63158de/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
@@ -206,12 +295,15 @@ golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 h1:CawjfCvYQH2OU3/TnxLx97WDSUDRABfT18pCOYwc2GE=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6/go.mod h1:3rxYc4HtVcSG9gVaTs2GEBdehh+sYPOwKtyUWEOTb80=
golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE=
golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de h1:cZGRis4/ot9uVm639a+rHCUaG0JJHEsdyzSQTMX+suY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY=
google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM=
@@ -221,7 +313,6 @@ google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHh
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
@@ -229,3 +320,5 @@ howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM=
howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE=
lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k=
software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=

View File

@@ -92,6 +92,7 @@ func EndpointRegistry() *endpoint.Registry {
registry := endpoint.NewRegistry()
registerWireGuardEndpoint(registry)
registerTailscaleEndpoint(registry)
return registry
}
@@ -110,6 +111,7 @@ func DNSTransportRegistry() *dns.TransportRegistry {
registerQUICTransports(registry)
registerDHCPTransport(registry)
registerTailscaleTransport(registry)
return registry
}

17
include/tailscale.go Normal file
View File

@@ -0,0 +1,17 @@
//go:build with_tailscale
package include
import (
"github.com/sagernet/sing-box/adapter/endpoint"
"github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/protocol/tailscale"
)
func registerTailscaleEndpoint(registry *endpoint.Registry) {
tailscale.RegisterEndpoint(registry)
}
func registerTailscaleTransport(registry *dns.TransportRegistry) {
tailscale.RegistryTransport(registry)
}

27
include/tailscale_stub.go Normal file
View File

@@ -0,0 +1,27 @@
//go:build !with_tailscale
package include
import (
"context"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/adapter/endpoint"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
)
func registerTailscaleEndpoint(registry *endpoint.Registry) {
endpoint.Register[option.TailscaleEndpointOptions](registry, C.TypeTailscale, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TailscaleEndpointOptions) (adapter.Endpoint, error) {
return nil, E.New(`Tailscale is not included in this build, rebuild with -tags with_tailscale`)
})
}
func registerTailscaleTransport(registry *dns.TransportRegistry) {
dns.RegisterTransport[option.TailscaleDNSServerOptions](registry, C.DNSTypeTailscale, func(ctx context.Context, logger log.ContextLogger, tag string, options option.TailscaleDNSServerOptions) (adapter.DNSTransport, error) {
return nil, E.New(`Tailscale is not included in this build, rebuild with -tags with_tailscale`)
})
}

View File

@@ -10,10 +10,6 @@ import (
E "github.com/sagernet/sing/common/exceptions"
)
const (
DefaultTimeFormat = "-0700 2006-01-02 15:04:05"
)
type Options struct {
Context context.Context
Options option.LogOptions
@@ -51,7 +47,7 @@ func New(options Options) (Factory, error) {
DisableColors: logOptions.DisableColor || logFilePath != "",
DisableTimestamp: !logOptions.Timestamp && logFilePath != "",
FullTimestamp: logOptions.Timestamp,
TimestampFormat: DefaultTimeFormat,
TimestampFormat: "-0700 2006-01-02 15:04:05",
}
factory := NewDefaultFactory(
options.Context,

View File

@@ -1,11 +0,0 @@
package mitm
import (
"encoding/base64"
"github.com/sagernet/sing/common"
)
var surgeTinyGif = common.OnceValue(func() []byte {
return common.Must1(base64.StdEncoding.DecodeString("R0lGODlhAQABAAAAACH5BAEAAAAALAAAAAABAAEAAAIBAAA="))
})

File diff suppressed because it is too large Load Diff

View File

@@ -127,6 +127,7 @@ nav:
- Endpoint:
- configuration/endpoint/index.md
- WireGuard: configuration/endpoint/wireguard.md
- Tailscale: configuration/endpoint/tailscale.md
- Inbound:
- configuration/inbound/index.md
- Direct: configuration/inbound/direct.md

View File

@@ -4,6 +4,7 @@ import (
"context"
"net/netip"
"net/url"
"os"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/experimental/deprecated"
@@ -120,13 +121,23 @@ func (o *NewDNSServerOptions) Upgrade(ctx context.Context) error {
if o.Type != C.DNSTypeLegacy {
return nil
}
defer func() {
encoder := json.NewEncoder(os.Stderr)
encoder.SetIndent("", " ")
encoder.Encode(o)
}()
options := o.Options.(*LegacyDNSServerOptions)
serverURL, _ := url.Parse(options.Address)
var serverType string
if serverURL.Scheme != "" {
serverType = serverURL.Scheme
} else {
serverType = C.DNSTypeUDP
switch options.Address {
case "local", "fakeip":
serverType = options.Address
default:
serverType = C.DNSTypeUDP
}
}
var remoteOptions RemoteDNSServerOptions
if options.Detour == "" {
@@ -158,6 +169,9 @@ func (o *NewDNSServerOptions) Upgrade(ctx context.Context) error {
}
}
switch serverType {
case C.DNSTypeLocal:
o.Type = C.DNSTypeLocal
o.Options = &remoteOptions.LocalDNSServerOptions
case C.DNSTypeUDP:
o.Type = C.DNSTypeUDP
o.Options = &remoteOptions
@@ -174,6 +188,8 @@ func (o *NewDNSServerOptions) Upgrade(ctx context.Context) error {
if serverAddr.Port != 0 && serverAddr.Port != 53 {
remoteOptions.ServerPort = serverAddr.Port
}
remoteOptions.Server = serverAddr.AddrString()
remoteOptions.ServerPort = serverAddr.Port
case C.DNSTypeTCP:
o.Type = C.DNSTypeTCP
o.Options = &remoteOptions
@@ -185,19 +201,20 @@ func (o *NewDNSServerOptions) Upgrade(ctx context.Context) error {
if serverAddr.Port != 0 && serverAddr.Port != 53 {
remoteOptions.ServerPort = serverAddr.Port
}
remoteOptions.Server = serverAddr.AddrString()
remoteOptions.ServerPort = serverAddr.Port
case C.DNSTypeTLS, C.DNSTypeQUIC:
o.Type = serverType
tlsOptions := RemoteTLSDNSServerOptions{
RemoteDNSServerOptions: remoteOptions,
}
o.Options = &tlsOptions
serverAddr := M.ParseSocksaddr(serverURL.Host)
if !serverAddr.IsValid() {
return E.New("invalid server address")
}
tlsOptions.Server = serverAddr.Addr.String()
remoteOptions.Server = serverAddr.Addr.String()
if serverAddr.Port != 0 && serverAddr.Port != 853 {
tlsOptions.ServerPort = serverAddr.Port
remoteOptions.ServerPort = serverAddr.Port
}
o.Options = &RemoteTLSDNSServerOptions{
RemoteDNSServerOptions: remoteOptions,
}
case C.DNSTypeHTTPS, C.DNSTypeHTTP3:
o.Type = serverType
@@ -244,14 +261,14 @@ func (o *NewDNSServerOptions) Upgrade(ctx context.Context) error {
},
},
}
case "dhcp":
case C.DNSTypeDHCP:
o.Type = C.DNSTypeDHCP
dhcpOptions := DHCPDNSServerOptions{}
if serverURL.Host != "" && serverURL.Host != "auto" {
dhcpOptions.Interface = serverURL.Host
}
o.Options = &dhcpOptions
case "fakeip":
case C.DNSTypeFakeIP:
o.Type = C.DNSTypeFakeIP
fakeipOptions := FakeIPDNSServerOptions{}
if legacyOptions, loaded := ctx.Value((*LegacyDNSFakeIPOptions)(nil)).(*LegacyDNSFakeIPOptions); loaded {

View File

@@ -1,38 +0,0 @@
package option
import (
"github.com/sagernet/sing/common/json/badoption"
)
type MITMOptions struct {
Enabled bool `json:"enabled,omitempty"`
HTTP2Enabled bool `json:"http2_enabled,omitempty"`
TLSDecryptionOptions *TLSDecryptionOptions `json:"tls_decryption,omitempty"`
}
type TLSDecryptionOptions struct {
Enabled bool `json:"enabled,omitempty"`
KeyPair string `json:"key_pair_p12,omitempty"`
KeyPassword string `json:"key_password,omitempty"`
}
type MITMRouteOptions struct {
Enabled bool `json:"enabled,omitempty"`
Print bool `json:"print,omitempty"`
Script badoption.Listable[MITMRouteSurgeScriptOptions] `json:"sg_script,omitempty"`
SurgeURLRewrite badoption.Listable[SurgeURLRewriteLine] `json:"sg_url_rewrite,omitempty"`
SurgeHeaderRewrite badoption.Listable[SurgeHeaderRewriteLine] `json:"sg_header_rewrite,omitempty"`
SurgeBodyRewrite badoption.Listable[SurgeBodyRewriteLine] `json:"sg_body_rewrite,omitempty"`
SurgeMapLocal badoption.Listable[SurgeMapLocalLine] `json:"sg_map_local,omitempty"`
}
type MITMRouteSurgeScriptOptions struct {
Tag string `json:"tag"`
Type badoption.Listable[string] `json:"type"`
Pattern badoption.Listable[*badoption.Regexp] `json:"pattern"`
Timeout badoption.Duration `json:"timeout,omitempty"`
RequiresBody bool `json:"requires_body,omitempty"`
MaxSize int64 `json:"max_size,omitempty"`
BinaryBodyMode bool `json:"binary_body_mode,omitempty"`
Arguments badoption.Listable[string] `json:"arguments,omitempty"`
}

View File

@@ -1,444 +0,0 @@
package option
import (
"encoding/base64"
"net/http"
"net/url"
"regexp"
"strconv"
"strings"
"unicode"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/common/json"
)
type SurgeURLRewriteLine struct {
Pattern *regexp.Regexp
Destination *url.URL
Redirect bool
Reject bool
}
func (l SurgeURLRewriteLine) String() string {
var fields []string
fields = append(fields, l.Pattern.String())
if l.Reject {
fields = append(fields, "_")
} else {
fields = append(fields, l.Destination.String())
}
switch {
case l.Redirect:
fields = append(fields, "302")
case l.Reject:
fields = append(fields, "reject")
default:
fields = append(fields, "header")
}
return encodeSurgeKeys(fields)
}
func (l SurgeURLRewriteLine) MarshalJSON() ([]byte, error) {
return json.Marshal(l.String())
}
func (l *SurgeURLRewriteLine) UnmarshalJSON(bytes []byte) error {
var stringValue string
err := json.Unmarshal(bytes, &stringValue)
if err != nil {
return err
}
fields, err := surgeFields(stringValue)
if err != nil {
return E.Cause(err, "invalid surge_url_rewrite line: ", stringValue)
} else if len(fields) < 2 || len(fields) > 3 {
return E.New("invalid surge_url_rewrite line: ", stringValue)
}
pattern, err := regexp.Compile(fields[0].Key)
if err != nil {
return E.Cause(err, "invalid surge_url_rewrite line: invalid pattern: ", stringValue)
}
l.Pattern = pattern
l.Destination, err = url.Parse(fields[1].Key)
if err != nil {
return E.Cause(err, "invalid surge_url_rewrite line: invalid destination: ", stringValue)
}
if len(fields) == 3 {
switch fields[2].Key {
case "header":
case "302":
l.Redirect = true
case "reject":
l.Reject = true
default:
return E.New("invalid surge_url_rewrite line: invalid action: ", stringValue)
}
}
return nil
}
type SurgeHeaderRewriteLine struct {
Response bool
Pattern *regexp.Regexp
Add bool
Delete bool
Replace bool
ReplaceRegex bool
Key string
Match *regexp.Regexp
Value string
}
func (l SurgeHeaderRewriteLine) String() string {
var fields []string
if !l.Response {
fields = append(fields, "http-request")
} else {
fields = append(fields, "http-response")
}
fields = append(fields, l.Pattern.String())
if l.Add {
fields = append(fields, "header-add")
} else if l.Delete {
fields = append(fields, "header-del")
} else if l.Replace {
fields = append(fields, "header-replace")
} else if l.ReplaceRegex {
fields = append(fields, "header-replace-regex")
}
fields = append(fields, l.Key)
if l.Add || l.Replace {
fields = append(fields, l.Value)
} else if l.ReplaceRegex {
fields = append(fields, l.Match.String(), l.Value)
}
return encodeSurgeKeys(fields)
}
func (l SurgeHeaderRewriteLine) MarshalJSON() ([]byte, error) {
return json.Marshal(l.String())
}
func (l *SurgeHeaderRewriteLine) UnmarshalJSON(bytes []byte) error {
var stringValue string
err := json.Unmarshal(bytes, &stringValue)
if err != nil {
return err
}
fields, err := surgeFields(stringValue)
if err != nil {
return E.Cause(err, "invalid surge_header_rewrite line: ", stringValue)
} else if len(fields) < 4 {
return E.New("invalid surge_header_rewrite line: ", stringValue)
}
switch fields[0].Key {
case "http-request":
case "http-response":
l.Response = true
default:
return E.New("invalid surge_header_rewrite line: invalid type: ", stringValue)
}
l.Pattern, err = regexp.Compile(fields[1].Key)
if err != nil {
return E.Cause(err, "invalid surge_header_rewrite line: invalid pattern: ", stringValue)
}
switch fields[2].Key {
case "header-add":
l.Add = true
if len(fields) != 5 {
return E.New("invalid surge_header_rewrite line: " + stringValue)
}
l.Key = fields[3].Key
l.Value = fields[4].Key
case "header-del":
l.Delete = true
l.Key = fields[3].Key
case "header-replace":
l.Replace = true
if len(fields) != 5 {
return E.New("invalid surge_header_rewrite line: " + stringValue)
}
l.Key = fields[3].Key
l.Value = fields[4].Key
case "header-replace-regex":
l.ReplaceRegex = true
if len(fields) != 6 {
return E.New("invalid surge_header_rewrite line: " + stringValue)
}
l.Key = fields[3].Key
l.Match, err = regexp.Compile(fields[4].Key)
if err != nil {
return E.Cause(err, "invalid surge_header_rewrite line: invalid match: ", stringValue)
}
l.Value = fields[5].Key
default:
return E.New("invalid surge_header_rewrite line: invalid action: ", stringValue)
}
return nil
}
type SurgeBodyRewriteLine struct {
Response bool
Pattern *regexp.Regexp
Match []*regexp.Regexp
Replace []string
}
func (l SurgeBodyRewriteLine) String() string {
var fields []string
if !l.Response {
fields = append(fields, "http-request")
} else {
fields = append(fields, "http-response")
}
for i := 0; i < len(l.Match); i += 2 {
fields = append(fields, l.Match[i].String(), l.Replace[i])
}
return strings.Join(fields, " ")
}
func (l SurgeBodyRewriteLine) MarshalJSON() ([]byte, error) {
return json.Marshal(l.String())
}
func (l *SurgeBodyRewriteLine) UnmarshalJSON(bytes []byte) error {
var stringValue string
err := json.Unmarshal(bytes, &stringValue)
if err != nil {
return err
}
fields, err := surgeFields(stringValue)
if err != nil {
return E.Cause(err, "invalid surge_body_rewrite line: ", stringValue)
} else if len(fields) < 4 {
return E.New("invalid surge_body_rewrite line: ", stringValue)
} else if len(fields)%2 != 0 {
return E.New("invalid surge_body_rewrite line: ", stringValue)
}
switch fields[0].Key {
case "http-request":
case "http-response":
l.Response = true
default:
return E.New("invalid surge_body_rewrite line: invalid type: ", stringValue)
}
l.Pattern, err = regexp.Compile(fields[1].Key)
for i := 2; i < len(fields); i += 2 {
var match *regexp.Regexp
match, err = regexp.Compile(fields[i].Key)
if err != nil {
return E.Cause(err, "invalid surge_body_rewrite line: invalid match: ", stringValue)
}
l.Match = append(l.Match, match)
l.Replace = append(l.Replace, fields[i+1].Key)
}
return nil
}
type SurgeMapLocalLine struct {
Pattern *regexp.Regexp
StatusCode int
File bool
Text bool
TinyGif bool
Base64 bool
Data string
Base64Data []byte
Headers http.Header
}
func (l SurgeMapLocalLine) String() string {
var fields []surgeField
fields = append(fields, surgeField{Key: l.Pattern.String()})
if l.File {
fields = append(fields, surgeField{Key: "data-type", Value: "file"})
fields = append(fields, surgeField{Key: "data", Value: l.Data})
} else if l.Text {
fields = append(fields, surgeField{Key: "data-type", Value: "text"})
fields = append(fields, surgeField{Key: "data", Value: l.Data})
} else if l.TinyGif {
fields = append(fields, surgeField{Key: "data-type", Value: "tiny-gif"})
} else if l.Base64 {
fields = append(fields, surgeField{Key: "data-type", Value: "base64"})
fields = append(fields, surgeField{Key: "data-type", Value: base64.StdEncoding.EncodeToString(l.Base64Data)})
}
fields = append(fields, surgeField{Key: "status-code", Value: F.ToString(l.StatusCode), ValueSet: true})
if len(l.Headers) > 0 {
var headers []string
for key, values := range l.Headers {
for _, value := range values {
headers = append(headers, key+":"+value)
}
}
fields = append(fields, surgeField{Key: "headers", Value: strings.Join(headers, "|")})
}
return encodeSurgeFields(fields)
}
func (l SurgeMapLocalLine) MarshalJSON() ([]byte, error) {
return json.Marshal(l.String())
}
func (l *SurgeMapLocalLine) UnmarshalJSON(bytes []byte) error {
var stringValue string
err := json.Unmarshal(bytes, &stringValue)
if err != nil {
return err
}
fields, err := surgeFields(stringValue)
if err != nil {
return E.Cause(err, "invalid surge_map_local line: ", stringValue)
} else if len(fields) < 1 {
return E.New("invalid surge_map_local line: ", stringValue)
}
l.Pattern, err = regexp.Compile(fields[0].Key)
if err != nil {
return E.Cause(err, "invalid surge_map_local line: invalid pattern: ", stringValue)
}
dataTypeField := common.Find(fields, func(it surgeField) bool {
return it.Key == "data-type"
})
if !dataTypeField.ValueSet {
return E.New("invalid surge_map_local line: missing data-type: ", stringValue)
}
switch dataTypeField.Value {
case "file":
l.File = true
case "text":
l.Text = true
case "tiny-gif":
l.TinyGif = true
case "base64":
l.Base64 = true
}
for i := 1; i < len(fields); i++ {
switch fields[i].Key {
case "data-type":
continue
case "data":
if l.File {
l.Data = fields[i].Value
} else if l.Text {
l.Data = fields[i].Value
} else if l.Base64 {
l.Base64Data, err = base64.StdEncoding.DecodeString(fields[i].Value)
if err != nil {
return E.New("invalid surge_map_local line: invalid base64 data: ", stringValue)
}
}
case "status-code":
statusCode, err := strconv.ParseInt(fields[i].Value, 10, 16)
if err != nil {
return E.New("invalid surge_map_local line: invalid status code: ", stringValue)
}
l.StatusCode = int(statusCode)
case "headers":
headers := make(http.Header)
for _, headerLine := range strings.Split(fields[i].Value, "|") {
if !strings.Contains(headerLine, ":") {
return E.New("invalid surge_map_local line: headers: missing `:` in item: ", stringValue, ": ", headerLine)
}
headers.Add(common.SubstringBefore(headerLine, ":"), common.SubstringAfter(headerLine, ":"))
}
l.Headers = headers
default:
return E.New("invalid surge_map_local line: unknown options: ", stringValue)
}
}
return nil
}
type surgeField struct {
Key string
Value string
ValueSet bool
}
func encodeSurgeKeys(keys []string) string {
keys = common.Map(keys, func(it string) string {
if strings.ContainsFunc(it, unicode.IsSpace) {
return "\"" + it + "\""
} else {
return it
}
})
return strings.Join(keys, " ")
}
func encodeSurgeFields(fields []surgeField) string {
return strings.Join(common.Map(fields, func(it surgeField) string {
if !it.ValueSet {
if strings.ContainsFunc(it.Key, unicode.IsSpace) {
return "\"" + it.Key + "\""
} else {
return it.Key
}
} else {
if strings.ContainsFunc(it.Value, unicode.IsSpace) {
return it.Key + "=\"" + it.Value + "\""
} else {
return it.Key + "=" + it.Value
}
}
}), " ")
}
func surgeFields(s string) ([]surgeField, error) {
var (
fields []surgeField
currentField *surgeField
)
for _, field := range strings.Fields(s) {
if currentField != nil {
field = " " + field
if strings.HasSuffix(field, "\"") {
field = field[:len(field)-1]
if !currentField.ValueSet {
currentField.Key += field
} else {
currentField.Value += field
}
fields = append(fields, *currentField)
currentField = nil
} else {
if !currentField.ValueSet {
currentField.Key += field
} else {
currentField.Value += " " + field
}
}
}
if !strings.Contains(field, "=") {
if strings.HasPrefix(field, "\"") {
field = field[1:]
if strings.HasSuffix(field, "\"") {
field = field[:len(field)-1]
} else {
currentField = &surgeField{Key: field}
continue
}
}
fields = append(fields, surgeField{Key: field})
} else {
key := common.SubstringBefore(field, "=")
value := common.SubstringAfter(field, "=")
if strings.HasPrefix(value, "\"") {
value = value[1:]
if strings.HasSuffix(field, "\"") {
value = value[:len(value)-1]
} else {
currentField = &surgeField{Key: key, Value: field, ValueSet: true}
continue
}
}
fields = append(fields, surgeField{Key: key, Value: value, ValueSet: true})
}
}
if currentField != nil {
return nil, E.New("invalid surge fields line: ", s)
}
return fields, nil
}

View File

@@ -12,15 +12,13 @@ type _Options struct {
Schema string `json:"$schema,omitempty"`
Log *LogOptions `json:"log,omitempty"`
DNS *DNSOptions `json:"dns,omitempty"`
NTP *NTPOptions `json:"ntp,omitempty"`
Certificate *CertificateOptions `json:"certificate,omitempty"`
Endpoints []Endpoint `json:"endpoints,omitempty"`
Inbounds []Inbound `json:"inbounds,omitempty"`
Outbounds []Outbound `json:"outbounds,omitempty"`
Route *RouteOptions `json:"route,omitempty"`
Experimental *ExperimentalOptions `json:"experimental,omitempty"`
NTP *NTPOptions `json:"ntp,omitempty"`
Certificate *CertificateOptions `json:"certificate,omitempty"`
MITM *MITMOptions `json:"mitm,omitempty"`
Scripts []Script `json:"scripts,omitempty"`
}
type Options _Options

View File

@@ -153,8 +153,6 @@ type RawRouteOptionsActionOptions struct {
TLSFragment bool `json:"tls_fragment,omitempty"`
TLSFragmentFallbackDelay badoption.Duration `json:"tls_fragment_fallback_delay,omitempty"`
MITM *MITMRouteOptions `json:"mitm,omitempty"`
}
type RouteOptionsActionOptions RawRouteOptionsActionOptions

View File

@@ -1,128 +0,0 @@
package option
import (
C "github.com/sagernet/sing-box/constant"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badjson"
"github.com/sagernet/sing/common/json/badoption"
)
type _ScriptSourceOptions struct {
Source string `json:"source"`
LocalOptions LocalScriptSource `json:"-"`
RemoteOptions RemoteScriptSource `json:"-"`
}
type LocalScriptSource struct {
Path string `json:"path"`
}
type RemoteScriptSource struct {
URL string `json:"url"`
DownloadDetour string `json:"download_detour,omitempty"`
UpdateInterval badoption.Duration `json:"update_interval,omitempty"`
}
type ScriptSourceOptions _ScriptSourceOptions
func (o ScriptSourceOptions) MarshalJSON() ([]byte, error) {
var source any
switch o.Source {
case C.ScriptSourceTypeLocal:
source = o.LocalOptions
case C.ScriptSourceTypeRemote:
source = o.RemoteOptions
default:
return nil, E.New("unknown script source: ", o.Source)
}
return badjson.MarshallObjects((_ScriptSourceOptions)(o), source)
}
func (o *ScriptSourceOptions) UnmarshalJSON(bytes []byte) error {
err := json.Unmarshal(bytes, (*_ScriptSourceOptions)(o))
if err != nil {
return err
}
var source any
switch o.Source {
case C.ScriptSourceTypeLocal:
source = &o.LocalOptions
case C.ScriptSourceTypeRemote:
source = &o.RemoteOptions
default:
return E.New("unknown script source: ", o.Source)
}
return json.Unmarshal(bytes, source)
}
// TODO: make struct in order
type Script struct {
ScriptSourceOptions
ScriptOptions
}
func (s Script) MarshalJSON() ([]byte, error) {
return badjson.MarshallObjects(s.ScriptSourceOptions, s.ScriptOptions)
}
func (s *Script) UnmarshalJSON(bytes []byte) error {
err := json.Unmarshal(bytes, &s.ScriptSourceOptions)
if err != nil {
return err
}
return badjson.UnmarshallExcluded(bytes, &s.ScriptSourceOptions, &s.ScriptOptions)
}
type _ScriptOptions struct {
Type string `json:"type"`
Tag string `json:"tag"`
SurgeOptions SurgeScriptOptions `json:"-"`
}
type ScriptOptions _ScriptOptions
func (o ScriptOptions) MarshalJSON() ([]byte, error) {
var v any
switch o.Type {
case C.ScriptTypeSurge:
v = &o.SurgeOptions
default:
return nil, E.New("unknown script type: ", o.Type)
}
if v == nil {
return badjson.MarshallObjects((_ScriptOptions)(o))
}
return badjson.MarshallObjects((_ScriptOptions)(o), v)
}
func (o *ScriptOptions) UnmarshalJSON(bytes []byte) error {
err := json.Unmarshal(bytes, (*_ScriptOptions)(o))
if err != nil {
return err
}
var v any
switch o.Type {
case C.ScriptTypeSurge:
v = &o.SurgeOptions
case "":
return E.New("missing script type")
default:
return E.New("unknown script type: ", o.Type)
}
if v == nil {
// check unknown fields
return json.UnmarshalDisallowUnknownFields(bytes, &_ScriptOptions{})
}
return badjson.UnmarshallExcluded(bytes, (*_ScriptOptions)(o), v)
}
type SurgeScriptOptions struct {
CronOptions *CronScriptOptions `json:"cron,omitempty"`
}
type CronScriptOptions struct {
Expression string `json:"expression"`
Arguments []string `json:"arguments,omitempty"`
Timeout badoption.Duration `json:"timeout,omitempty"`
}

24
option/tailscale.go Normal file
View File

@@ -0,0 +1,24 @@
package option
import (
"net/netip"
)
type TailscaleEndpointOptions struct {
DialerOptions
StateDirectory string `json:"state_directory,omitempty"`
AuthKey string `json:"auth_key,omitempty"`
ControlURL string `json:"control_url,omitempty"`
Ephemeral bool `json:"ephemeral,omitempty"`
Hostname string `json:"hostname,omitempty"`
ExitNode string `json:"exit_node,omitempty"`
ExitNodeAllowLANAccess bool `json:"exit_node_allow_lan_access,omitempty"`
AdvertiseRoutes []netip.Prefix `json:"advertise_routes,omitempty"`
AdvertiseExitNode bool `json:"advertise_exit_node,omitempty"`
UDPTimeout UDPTimeoutCompat `json:"udp_timeout,omitempty"`
}
type TailscaleDNSServerOptions struct {
Endpoint string `json:"endpoint,omitempty"`
AcceptDefaultResolvers bool `json:"accept_default_resolvers,omitempty"`
}

View File

@@ -0,0 +1,322 @@
package tailscale
import (
"context"
"net"
"net/http"
"net/netip"
"net/url"
"os"
"reflect"
"strings"
"sync"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/dialer"
"github.com/sagernet/sing-box/common/tls"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/dns/transport"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/service"
nDNS "github.com/sagernet/tailscale/net/dns"
"github.com/sagernet/tailscale/types/dnstype"
"github.com/sagernet/tailscale/wgengine/router"
"github.com/sagernet/tailscale/wgengine/wgcfg"
mDNS "github.com/miekg/dns"
"go4.org/netipx"
"golang.org/x/net/http2"
)
func RegistryTransport(registry *dns.TransportRegistry) {
dns.RegisterTransport[option.TailscaleDNSServerOptions](registry, C.DNSTypeTailscale, NewDNSTransport)
}
type DNSTransport struct {
dns.TransportAdapter
ctx context.Context
logger logger.ContextLogger
endpointTag string
acceptDefaultResolvers bool
dnsRouter adapter.DNSRouter
endpointManager adapter.EndpointManager
cfg *wgcfg.Config
dnsCfg *nDNS.Config
endpoint *Endpoint
routePrefixes []netip.Prefix
routes map[string][]adapter.DNSTransport
hosts map[string][]netip.Addr
defaultResolvers []adapter.DNSTransport
}
func NewDNSTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.TailscaleDNSServerOptions) (adapter.DNSTransport, error) {
if options.Endpoint == "" {
return nil, E.New("missing tailscale endpoint tag")
}
return &DNSTransport{
TransportAdapter: dns.NewTransportAdapter(C.DNSTypeTailscale, tag, nil),
ctx: ctx,
logger: logger,
endpointTag: options.Endpoint,
acceptDefaultResolvers: options.AcceptDefaultResolvers,
dnsRouter: service.FromContext[adapter.DNSRouter](ctx),
endpointManager: service.FromContext[adapter.EndpointManager](ctx),
}, nil
}
func (t *DNSTransport) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateInitialize {
return nil
}
rawOutbound, loaded := t.endpointManager.Get(t.endpointTag)
if !loaded {
return E.New("endpoint not found: ", t.endpointTag)
}
ep, isTailscale := rawOutbound.(*Endpoint)
if !isTailscale {
return E.New("endpoint is not tailscale: ", t.endpointTag)
}
if ep.onReconfig != nil {
return E.New("only one tailscale DNS server is allowed for single endpoint")
}
ep.onReconfig = t.onReconfig
t.endpoint = ep
return nil
}
func (t *DNSTransport) Reset() {
}
func (t *DNSTransport) onReconfig(cfg *wgcfg.Config, routerCfg *router.Config, dnsCfg *nDNS.Config) {
if cfg == nil || dnsCfg == nil {
return
}
if (t.cfg != nil && reflect.DeepEqual(t.cfg, cfg)) && (t.dnsCfg != nil && reflect.DeepEqual(t.dnsCfg, dnsCfg)) {
return
}
t.cfg = cfg
t.dnsCfg = dnsCfg
err := t.updateDNSServers(routerCfg, dnsCfg)
if err != nil {
t.logger.Error(E.Cause(err, "update DNS servers"))
}
}
func (t *DNSTransport) updateDNSServers(routeConfig *router.Config, dnsConfig *nDNS.Config) error {
t.routePrefixes = buildRoutePrefixes(routeConfig)
directDialerOnce := sync.OnceValue(func() N.Dialer {
directDialer := common.Must1(dialer.NewDefault(t.ctx, option.DialerOptions{}))
return &DNSDialer{transport: t, fallbackDialer: directDialer}
})
routes := make(map[string][]adapter.DNSTransport)
for domain, resolvers := range dnsConfig.Routes {
var myResolvers []adapter.DNSTransport
for _, resolver := range resolvers {
myResolver, err := t.createResolver(directDialerOnce, resolver)
if err != nil {
return err
}
myResolvers = append(myResolvers, myResolver)
}
routes[domain.WithTrailingDot()] = myResolvers
}
hosts := make(map[string][]netip.Addr)
for domain, addresses := range dnsConfig.Hosts {
hosts[domain.WithTrailingDot()] = addresses
}
var defaultResolvers []adapter.DNSTransport
for _, resolver := range dnsConfig.DefaultResolvers {
t.logger.Warn("create default resolver: ", resolver.Addr)
myResolver, err := t.createResolver(directDialerOnce, resolver)
if err != nil {
return err
}
defaultResolvers = append(defaultResolvers, myResolver)
}
t.logger.Error("create ", len(dnsConfig.DefaultResolvers), " default resolvers")
t.routes = routes
t.hosts = hosts
t.defaultResolvers = defaultResolvers
if len(defaultResolvers) > 0 {
t.logger.Info("updated ", len(routes), " routes, ", len(hosts), " hosts, default resolvers: ",
strings.Join(common.Map(dnsConfig.DefaultResolvers, func(it *dnstype.Resolver) string { return it.Addr }), " "))
} else {
t.logger.Info("updated ", len(routes), " routes, ", len(hosts), " hosts")
}
return nil
}
func (t *DNSTransport) createResolver(directDialer func() N.Dialer, resolver *dnstype.Resolver) (adapter.DNSTransport, error) {
serverURL, parseURLErr := url.Parse(resolver.Addr)
var myDialer N.Dialer
if parseURLErr == nil && serverURL.Scheme == "http" {
myDialer = t.endpoint
} else {
myDialer = directDialer()
}
if len(resolver.BootstrapResolution) > 0 {
bootstrapTransport := transport.NewUDPRaw(t.logger, t.TransportAdapter, myDialer, M.SocksaddrFrom(resolver.BootstrapResolution[0], 53))
myDialer = dialer.NewResolveDialer(t.ctx, myDialer, false, "", adapter.DNSQueryOptions{Transport: bootstrapTransport}, 0)
}
if serverAddr := M.ParseSocksaddr(resolver.Addr); serverAddr.IsValid() {
if serverAddr.Port == 0 {
serverAddr.Port = 53
}
return transport.NewUDPRaw(t.logger, t.TransportAdapter, myDialer, serverAddr), nil
} else if parseURLErr != nil {
return nil, E.Cause(parseURLErr, "parse resolver address")
} else {
switch serverURL.Scheme {
case "https":
serverAddr = M.ParseSocksaddrHostPortStr(serverURL.Hostname(), serverURL.Port())
if serverAddr.Port == 0 {
serverAddr.Port = 443
}
tlsConfig := common.Must1(tls.NewClient(t.ctx, serverAddr.AddrString(), option.OutboundTLSOptions{
ALPN: []string{http2.NextProtoTLS, "http/1.1"},
}))
return transport.NewHTTPSRaw(t.TransportAdapter, t.logger, myDialer, serverURL, http.Header{}, serverAddr, tlsConfig), nil
case "http":
serverAddr = M.ParseSocksaddrHostPortStr(serverURL.Hostname(), serverURL.Port())
if serverAddr.Port == 0 {
serverAddr.Port = 80
}
return transport.NewHTTPSRaw(t.TransportAdapter, t.logger, myDialer, serverURL, http.Header{}, serverAddr, nil), nil
// case "tls":
default:
return nil, E.New("unknown resolver scheme: ", serverURL.Scheme)
}
}
}
func buildRoutePrefixes(routeConfig *router.Config) []netip.Prefix {
var builder netipx.IPSetBuilder
for _, localAddr := range routeConfig.LocalAddrs {
builder.AddPrefix(localAddr)
}
for _, route := range routeConfig.Routes {
builder.AddPrefix(route)
}
for _, route := range routeConfig.LocalRoutes {
builder.AddPrefix(route)
}
for _, route := range routeConfig.SubnetRoutes {
builder.AddPrefix(route)
}
ipSet, err := builder.IPSet()
if err != nil {
return nil
}
return ipSet.Prefixes()
}
func (t *DNSTransport) Close() error {
return nil
}
func (t *DNSTransport) Raw() bool {
return true
}
func (t *DNSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
if len(message.Question) != 1 {
return nil, os.ErrInvalid
}
question := message.Question[0]
addresses, hostsLoaded := t.hosts[question.Name]
if hostsLoaded {
switch question.Qtype {
case mDNS.TypeA:
addresses4 := common.Filter(addresses, func(addr netip.Addr) bool {
return addr.Is4()
})
if len(addresses4) > 0 {
return dns.FixedResponse(message.Id, question, addresses4, C.DefaultDNSTTL), nil
}
case mDNS.TypeAAAA:
addresses6 := common.Filter(addresses, func(addr netip.Addr) bool {
return addr.Is6()
})
if len(addresses6) > 0 {
return dns.FixedResponse(message.Id, question, addresses6, C.DefaultDNSTTL), nil
}
}
}
for domainSuffix, transports := range t.routes {
if strings.HasSuffix(question.Name, domainSuffix) {
if len(transports) == 0 {
return &mDNS.Msg{
MsgHdr: mDNS.MsgHdr{
Id: message.Id,
Rcode: mDNS.RcodeNameError,
Response: true,
},
Question: []mDNS.Question{question},
}, nil
}
var lastErr error
for _, dnsTransport := range transports {
response, err := dnsTransport.Exchange(ctx, message)
if err != nil {
lastErr = err
continue
}
return response, nil
}
return nil, lastErr
}
}
if t.acceptDefaultResolvers {
if len(t.defaultResolvers) > 0 {
var lastErr error
for _, resolver := range t.defaultResolvers {
response, err := resolver.Exchange(ctx, message)
if err != nil {
lastErr = err
continue
}
return response, nil
}
return nil, lastErr
} else {
return nil, E.New("missing default resolvers")
}
}
return nil, dns.RCodeNameError
}
type DNSDialer struct {
transport *DNSTransport
fallbackDialer N.Dialer
}
func (d *DNSDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
if destination.IsFqdn() {
panic("invalid request here")
}
for _, prefix := range d.transport.routePrefixes {
if prefix.Contains(destination.Addr) {
return d.transport.endpoint.DialContext(ctx, network, destination)
}
}
return d.fallbackDialer.DialContext(ctx, network, destination)
}
func (d *DNSDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
if destination.IsFqdn() {
panic("invalid request here")
}
for _, prefix := range d.transport.routePrefixes {
if prefix.Contains(destination.Addr) {
return d.transport.endpoint.ListenPacket(ctx, destination)
}
}
return d.fallbackDialer.ListenPacket(ctx, destination)
}

View File

@@ -0,0 +1,472 @@
package tailscale
import (
"context"
"fmt"
"net"
"net/netip"
"net/url"
"os"
"path/filepath"
"runtime"
"strings"
"sync/atomic"
"syscall"
"time"
"github.com/sagernet/gvisor/pkg/tcpip"
"github.com/sagernet/gvisor/pkg/tcpip/adapters/gonet"
"github.com/sagernet/gvisor/pkg/tcpip/header"
"github.com/sagernet/gvisor/pkg/tcpip/stack"
"github.com/sagernet/gvisor/pkg/tcpip/transport/tcp"
"github.com/sagernet/gvisor/pkg/tcpip/transport/udp"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/adapter/endpoint"
"github.com/sagernet/sing-box/common/dialer"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/experimental/libbox/platform"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/bufio"
"github.com/sagernet/sing/common/control"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/service"
"github.com/sagernet/sing/service/filemanager"
"github.com/sagernet/tailscale/ipn"
"github.com/sagernet/tailscale/net/netmon"
"github.com/sagernet/tailscale/net/tsaddr"
"github.com/sagernet/tailscale/tsnet"
"github.com/sagernet/tailscale/types/ipproto"
"github.com/sagernet/tailscale/version"
"github.com/sagernet/tailscale/wgengine"
"github.com/sagernet/tailscale/wgengine/filter"
)
func init() {
version.SetVersion("sing-box " + C.Version)
}
func RegisterEndpoint(registry *endpoint.Registry) {
endpoint.Register[option.TailscaleEndpointOptions](registry, C.TypeTailscale, NewEndpoint)
}
type Endpoint struct {
endpoint.Adapter
ctx context.Context
router adapter.Router
logger logger.ContextLogger
dnsRouter adapter.DNSRouter
network adapter.NetworkManager
platformInterface platform.Interface
server *tsnet.Server
stack *stack.Stack
filter *atomic.Pointer[filter.Filter]
onReconfig wgengine.ReconfigListener
exitNode string
exitNodeAllowLANAccess bool
advertiseRoutes []netip.Prefix
advertiseExitNode bool
udpTimeout time.Duration
}
func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TailscaleEndpointOptions) (adapter.Endpoint, error) {
stateDirectory := options.StateDirectory
if stateDirectory == "" {
stateDirectory = "tailscale"
}
hostname := options.Hostname
if hostname == "" {
osHostname, _ := os.Hostname()
osHostname = strings.TrimSpace(osHostname)
hostname = osHostname
}
if hostname == "" {
hostname = "sing-box"
}
stateDirectory = filemanager.BasePath(ctx, os.ExpandEnv(stateDirectory))
stateDirectory, _ = filepath.Abs(stateDirectory)
for _, advertiseRoute := range options.AdvertiseRoutes {
if advertiseRoute.Addr().IsUnspecified() && advertiseRoute.Bits() == 0 {
return nil, E.New("`advertise_routes` cannot be default, use `advertise_exit_node` instead.")
}
}
if options.AdvertiseExitNode && options.ExitNode != "" {
return nil, E.New("cannot advertise an exit node and use an exit node at the same time.")
}
var udpTimeout time.Duration
if options.UDPTimeout != 0 {
udpTimeout = time.Duration(options.UDPTimeout)
} else {
udpTimeout = C.UDPTimeout
}
var remoteIsDomain bool
if options.ControlURL != "" {
controlURL, err := url.Parse(options.ControlURL)
if err != nil {
return nil, E.Cause(err, "parse control URL")
}
remoteIsDomain = M.IsDomainName(controlURL.Hostname())
} else {
// controlplane.tailscale.com
remoteIsDomain = true
}
outboundDialer, err := dialer.NewWithOptions(dialer.Options{
Context: ctx,
Options: options.DialerOptions,
RemoteIsDomain: remoteIsDomain,
NewDialer: true,
})
if err != nil {
return nil, err
}
dnsRouter := service.FromContext[adapter.DNSRouter](ctx)
server := &tsnet.Server{
Dir: stateDirectory,
Hostname: hostname,
Logf: func(format string, args ...any) {
logger.Trace(fmt.Sprintf(format, args...))
},
UserLogf: func(format string, args ...any) {
logger.Debug(fmt.Sprintf(format, args...))
},
Ephemeral: options.Ephemeral,
AuthKey: options.AuthKey,
ControlURL: options.ControlURL,
Dialer: &endpointDialer{Dialer: outboundDialer, logger: logger},
LookupHook: func(ctx context.Context, host string) ([]netip.Addr, error) {
return dnsRouter.Lookup(ctx, host, outboundDialer.(dialer.ResolveDialer).QueryOptions())
},
}
return &Endpoint{
Adapter: endpoint.NewAdapter(C.TypeTailscale, tag, []string{N.NetworkTCP, N.NetworkUDP}, nil),
ctx: ctx,
router: router,
logger: logger,
dnsRouter: dnsRouter,
network: service.FromContext[adapter.NetworkManager](ctx),
platformInterface: service.FromContext[platform.Interface](ctx),
server: server,
exitNode: options.ExitNode,
exitNodeAllowLANAccess: options.ExitNodeAllowLANAccess,
advertiseRoutes: options.AdvertiseRoutes,
advertiseExitNode: options.AdvertiseExitNode,
udpTimeout: udpTimeout,
}, nil
}
func (t *Endpoint) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateStart {
return nil
}
if t.platformInterface != nil {
err := t.network.UpdateInterfaces()
if err != nil {
return err
}
netmon.RegisterInterfaceGetter(func() ([]netmon.Interface, error) {
return common.Map(t.network.InterfaceFinder().Interfaces(), func(it control.Interface) netmon.Interface {
return netmon.Interface{
Interface: &net.Interface{
Index: it.Index,
MTU: it.MTU,
Name: it.Name,
HardwareAddr: it.HardwareAddr,
Flags: it.Flags,
},
AltAddrs: common.Map(it.Addresses, func(it netip.Prefix) net.Addr {
return &net.IPNet{
IP: it.Addr().AsSlice(),
Mask: net.CIDRMask(it.Bits(), it.Addr().BitLen()),
}
}),
}
}), nil
})
if runtime.GOOS == "android" {
setAndroidProtectFunc(t.platformInterface)
}
}
err := t.server.Start()
if err != nil {
return err
}
if t.onReconfig != nil {
t.server.ExportLocalBackend().ExportEngine().(wgengine.ExportedUserspaceEngine).SetOnReconfigListener(t.onReconfig)
}
ipStack := t.server.ExportNetstack().ExportIPStack()
ipStack.SetTransportProtocolHandler(tcp.ProtocolNumber, tun.NewTCPForwarder(t.ctx, ipStack, t).HandlePacket)
udpForwarder := tun.NewUDPForwarder(t.ctx, ipStack, t, t.udpTimeout)
ipStack.SetTransportProtocolHandler(udp.ProtocolNumber, udpForwarder.HandlePacket)
t.stack = ipStack
localBackend := t.server.ExportLocalBackend()
perfs := &ipn.MaskedPrefs{
ExitNodeIPSet: true,
AdvertiseRoutesSet: true,
}
if len(t.advertiseRoutes) > 0 {
perfs.AdvertiseRoutes = t.advertiseRoutes
}
if t.advertiseExitNode {
perfs.AdvertiseRoutes = append(perfs.AdvertiseRoutes, tsaddr.ExitRoutes()...)
}
_, err = localBackend.EditPrefs(perfs)
if err != nil {
return E.Cause(err, "update prefs")
}
t.filter = localBackend.ExportFilter()
go t.watchState()
return nil
}
func (t *Endpoint) watchState() {
localBackend := t.server.ExportLocalBackend()
localBackend.WatchNotifications(t.ctx, ipn.NotifyInitialState, nil, func(roNotify *ipn.Notify) (keepGoing bool) {
if roNotify.State != nil && *roNotify.State != ipn.NeedsLogin && *roNotify.State != ipn.NoState {
return false
}
authURL := localBackend.StatusWithoutPeers().AuthURL
if authURL != "" {
t.logger.Info("Waiting for authentication: ", authURL)
if t.platformInterface != nil {
err := t.platformInterface.SendNotification(&platform.Notification{
Identifier: "tailscale-authentication",
TypeName: "Tailscale Authentication Notifications",
TypeID: 10,
Title: "Tailscale Authentication",
Body: F.ToString("Tailscale outbound[", t.Tag(), "] is waiting for authentication."),
OpenURL: authURL,
})
if err != nil {
t.logger.Error("send authentication notification: ", err)
}
}
return false
}
return true
})
if t.exitNode != "" {
localBackend.WatchNotifications(t.ctx, ipn.NotifyInitialState, nil, func(roNotify *ipn.Notify) (keepGoing bool) {
if roNotify.State == nil || *roNotify.State != ipn.Running {
return true
}
status, err := common.Must1(t.server.LocalClient()).Status(t.ctx)
if err != nil {
t.logger.Error("set exit node: ", err)
return
}
perfs := &ipn.MaskedPrefs{
Prefs: ipn.Prefs{
ExitNodeAllowLANAccess: t.exitNodeAllowLANAccess,
},
ExitNodeIPSet: true,
ExitNodeAllowLANAccessSet: true,
}
err = perfs.SetExitNodeIP(t.exitNode, status)
if err != nil {
t.logger.Error("set exit node: ", err)
return true
}
_, err = localBackend.EditPrefs(perfs)
if err != nil {
t.logger.Error("set exit node: ", err)
return true
}
return false
})
}
}
func (t *Endpoint) Close() error {
netmon.RegisterInterfaceGetter(nil)
if runtime.GOOS == "android" {
setAndroidProtectFunc(nil)
}
return common.Close(common.PtrOrNil(t.server))
}
func (t *Endpoint) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
switch network {
case N.NetworkTCP:
t.logger.InfoContext(ctx, "outbound connection to ", destination)
case N.NetworkUDP:
t.logger.InfoContext(ctx, "outbound packet connection to ", destination)
}
if destination.IsFqdn() {
destinationAddresses, err := t.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{})
if err != nil {
return nil, err
}
return N.DialSerial(ctx, t, network, destination, destinationAddresses)
}
addr := tcpip.FullAddress{
NIC: 1,
Port: destination.Port,
Addr: addressFromAddr(destination.Addr),
}
var networkProtocol tcpip.NetworkProtocolNumber
if destination.IsIPv4() {
networkProtocol = header.IPv4ProtocolNumber
} else {
networkProtocol = header.IPv6ProtocolNumber
}
switch N.NetworkName(network) {
case N.NetworkTCP:
tcpConn, err := gonet.DialContextTCP(ctx, t.stack, addr, networkProtocol)
if err != nil {
return nil, err
}
return tcpConn, nil
case N.NetworkUDP:
udpConn, err := gonet.DialUDP(t.stack, nil, &addr, networkProtocol)
if err != nil {
return nil, err
}
return udpConn, nil
default:
return nil, E.Extend(N.ErrUnknownNetwork, network)
}
}
func (t *Endpoint) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
t.logger.InfoContext(ctx, "outbound packet connection to ", destination)
if destination.IsFqdn() {
destinationAddresses, err := t.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{})
if err != nil {
return nil, err
}
packetConn, _, err := N.ListenSerial(ctx, t, destination, destinationAddresses)
if err != nil {
return nil, err
}
return packetConn, err
}
addr4, addr6 := t.server.TailscaleIPs()
bind := tcpip.FullAddress{
NIC: 1,
}
var networkProtocol tcpip.NetworkProtocolNumber
if destination.IsIPv4() {
if !addr4.IsValid() {
return nil, E.New("missing Tailscale IPv4 address")
}
networkProtocol = header.IPv4ProtocolNumber
bind.Addr = addressFromAddr(addr4)
} else {
if !addr6.IsValid() {
return nil, E.New("missing Tailscale IPv6 address")
}
networkProtocol = header.IPv6ProtocolNumber
bind.Addr = addressFromAddr(addr6)
}
udpConn, err := gonet.DialUDP(t.stack, &bind, nil, networkProtocol)
if err != nil {
return nil, err
}
return udpConn, nil
}
func (t *Endpoint) PrepareConnection(network string, source M.Socksaddr, destination M.Socksaddr) error {
tsFilter := t.filter.Load()
if tsFilter != nil {
var ipProto ipproto.Proto
switch N.NetworkName(network) {
case N.NetworkTCP:
ipProto = ipproto.TCP
case N.NetworkUDP:
ipProto = ipproto.UDP
}
response := tsFilter.Check(source.Addr, destination.Addr, destination.Port, ipProto)
switch response {
case filter.Drop:
return syscall.ECONNRESET
case filter.DropSilently:
return tun.ErrDrop
}
}
return t.router.PreMatch(adapter.InboundContext{
Inbound: t.Tag(),
InboundType: t.Type(),
Network: network,
Source: source,
Destination: destination,
})
}
func (t *Endpoint) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
var metadata adapter.InboundContext
metadata.Inbound = t.Tag()
metadata.InboundType = t.Type()
metadata.Source = source
addr4, addr6 := t.server.TailscaleIPs()
switch destination.Addr {
case addr4:
destination.Addr = netip.AddrFrom4([4]uint8{127, 0, 0, 1})
case addr6:
destination.Addr = netip.IPv6Loopback()
}
metadata.Destination = destination
t.logger.InfoContext(ctx, "inbound connection from ", source)
t.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination)
t.router.RouteConnectionEx(ctx, conn, metadata, onClose)
}
func (t *Endpoint) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
var metadata adapter.InboundContext
metadata.Inbound = t.Tag()
metadata.InboundType = t.Type()
metadata.Source = source
metadata.Destination = destination
addr4, addr6 := t.server.TailscaleIPs()
switch destination.Addr {
case addr4:
metadata.OriginDestination = destination
destination.Addr = netip.AddrFrom4([4]uint8{127, 0, 0, 1})
conn = bufio.NewNATPacketConn(bufio.NewNetPacketConn(conn), metadata.OriginDestination, metadata.Destination)
case addr6:
metadata.OriginDestination = destination
destination.Addr = netip.IPv6Loopback()
conn = bufio.NewNATPacketConn(bufio.NewNetPacketConn(conn), metadata.OriginDestination, metadata.Destination)
}
t.logger.InfoContext(ctx, "inbound packet connection from ", source)
t.logger.InfoContext(ctx, "inbound packet connection to ", destination)
t.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose)
}
func addressFromAddr(destination netip.Addr) tcpip.Address {
if destination.Is6() {
return tcpip.AddrFrom16(destination.As16())
} else {
return tcpip.AddrFrom4(destination.As4())
}
}
type endpointDialer struct {
N.Dialer
logger logger.ContextLogger
}
func (d *endpointDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
switch N.NetworkName(network) {
case N.NetworkTCP:
d.logger.InfoContext(ctx, "output connection to ", destination)
case N.NetworkUDP:
d.logger.InfoContext(ctx, "output packet connection to ", destination)
}
return d.Dialer.DialContext(ctx, network, destination)
}
func (d *endpointDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
d.logger.InfoContext(ctx, "output packet connection")
return d.Dialer.ListenPacket(ctx, destination)
}

View File

@@ -0,0 +1,16 @@
package tailscale
import (
"github.com/sagernet/sing-box/experimental/libbox/platform"
"github.com/sagernet/tailscale/net/netns"
)
func setAndroidProtectFunc(platformInterface platform.Interface) {
if platformInterface != nil {
netns.SetAndroidProtectFunc(func(fd int) error {
return platformInterface.AutoDetectInterfaceControl(fd)
})
} else {
netns.SetAndroidProtectFunc(nil)
}
}

View File

@@ -0,0 +1,8 @@
//go:build !android
package tailscale
import "github.com/sagernet/sing-box/experimental/libbox/platform"
func setAndroidProtectFunc(platformInterface platform.Interface) {
}

View File

@@ -21,31 +21,23 @@ import (
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/x/list"
"github.com/sagernet/sing/service"
)
var _ adapter.ConnectionManager = (*ConnectionManager)(nil)
type ConnectionManager struct {
ctx context.Context
logger logger.ContextLogger
mitm adapter.MITMEngine
access sync.Mutex
connections list.List[io.Closer]
}
func NewConnectionManager(ctx context.Context, logger logger.ContextLogger) *ConnectionManager {
func NewConnectionManager(logger logger.ContextLogger) *ConnectionManager {
return &ConnectionManager{
ctx: ctx,
logger: logger,
}
}
func (m *ConnectionManager) Start(stage adapter.StartStage) error {
switch stage {
case adapter.StartStateInitialize:
m.mitm = service.FromContext[adapter.MITMEngine](m.ctx)
}
return nil
}
@@ -60,14 +52,6 @@ func (m *ConnectionManager) Close() error {
}
func (m *ConnectionManager) NewConnection(ctx context.Context, this N.Dialer, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
if metadata.MITM != nil && metadata.MITM.Enabled {
if m.mitm == nil {
m.logger.WarnContext(ctx, "MITM disabled")
} else {
m.mitm.NewConnection(ctx, this, conn, metadata, onClose)
return
}
}
ctx = adapter.WithContext(ctx, &metadata)
var (
remoteConn net.Conn

View File

@@ -458,9 +458,6 @@ match:
metadata.TLSFragment = true
metadata.TLSFragmentFallbackDelay = routeOptions.TLSFragmentFallbackDelay
}
if routeOptions.MITM != nil && routeOptions.MITM.Enabled {
metadata.MITM = routeOptions.MITM
}
}
switch action := currentRule.Action().(type) {
case *rule.RuleActionSniff:
@@ -496,12 +493,8 @@ match:
break match
}
}
if !preMatch && inputPacketConn != nil && !metadata.Destination.IsFqdn() && !metadata.Destination.Addr.IsGlobalUnicast() {
var timeout time.Duration
if metadata.InboundType == C.TypeSOCKS {
timeout = C.TCPTimeout
}
newBuffer, newPacketBuffers, newErr := r.actionSniff(ctx, metadata, &rule.RuleActionSniff{Timeout: timeout}, inputConn, inputPacketConn)
if !preMatch && inputPacketConn != nil && (metadata.InboundType == C.TypeSOCKS || metadata.InboundType == C.TypeMixed) && !metadata.Destination.IsFqdn() && !metadata.Destination.Addr.IsGlobalUnicast() {
newBuffer, newPacketBuffers, newErr := r.actionSniff(ctx, metadata, &rule.RuleActionSniff{Timeout: C.TCPTimeout}, inputConn, inputPacketConn)
if newErr != nil {
fatalErr = newErr
return
@@ -597,7 +590,7 @@ func (r *Router) actionSniff(
return
}
} else {
if !metadata.Destination.Addr.IsGlobalUnicast() && !metadata.RouteOriginalDestination.IsValid() {
if (metadata.InboundType == C.TypeSOCKS || metadata.InboundType == C.TypeMixed) && !metadata.Destination.IsFqdn() && !metadata.Destination.Addr.IsGlobalUnicast() && !metadata.RouteOriginalDestination.IsValid() {
metadata.Destination = destination
}
if len(packetBuffers) > 0 {

View File

@@ -38,7 +38,6 @@ func NewRuleAction(ctx context.Context, logger logger.ContextLogger, action opti
UDPConnect: action.RouteOptions.UDPConnect,
TLSFragment: action.RouteOptions.TLSFragment,
TLSFragmentFallbackDelay: time.Duration(action.RouteOptions.TLSFragmentFallbackDelay),
MITM: action.RouteOptions.MITM,
},
}, nil
case C.RuleActionTypeRouteOptions:
@@ -52,7 +51,6 @@ func NewRuleAction(ctx context.Context, logger logger.ContextLogger, action opti
UDPTimeout: time.Duration(action.RouteOptionsOptions.UDPTimeout),
TLSFragment: action.RouteOptionsOptions.TLSFragment,
TLSFragmentFallbackDelay: time.Duration(action.RouteOptionsOptions.TLSFragmentFallbackDelay),
MITM: action.RouteOptionsOptions.MITM,
}, nil
case C.RuleActionTypeDirect:
directDialer, err := dialer.New(ctx, option.DialerOptions(action.DirectOptions), false)
@@ -142,7 +140,15 @@ func (r *RuleActionRoute) Type() string {
func (r *RuleActionRoute) String() string {
var descriptions []string
descriptions = append(descriptions, r.Outbound)
descriptions = append(descriptions, r.Descriptions()...)
if r.UDPDisableDomainUnmapping {
descriptions = append(descriptions, "udp-disable-domain-unmapping")
}
if r.UDPConnect {
descriptions = append(descriptions, "udp-connect")
}
if r.TLSFragment {
descriptions = append(descriptions, "tls-fragment")
}
return F.ToString("route(", strings.Join(descriptions, ","), ")")
}
@@ -158,14 +164,13 @@ type RuleActionRouteOptions struct {
UDPTimeout time.Duration
TLSFragment bool
TLSFragmentFallbackDelay time.Duration
MITM *option.MITMRouteOptions
}
func (r *RuleActionRouteOptions) Type() string {
return C.RuleActionTypeRouteOptions
}
func (r *RuleActionRouteOptions) Descriptions() []string {
func (r *RuleActionRouteOptions) String() string {
var descriptions []string
if r.OverrideAddress.IsValid() {
descriptions = append(descriptions, F.ToString("override-address=", r.OverrideAddress.AddrString()))
@@ -192,22 +197,9 @@ func (r *RuleActionRouteOptions) Descriptions() []string {
descriptions = append(descriptions, "udp-connect")
}
if r.UDPTimeout > 0 {
descriptions = append(descriptions, F.ToString("udp-timeout=", r.UDPTimeout))
descriptions = append(descriptions, "udp-timeout")
}
if r.TLSFragment {
descriptions = append(descriptions, "tls-fragment")
if r.TLSFragmentFallbackDelay > 0 {
descriptions = append(descriptions, F.ToString("tls-fragment-fallbac-delay=", r.TLSFragmentFallbackDelay.String()))
}
}
if r.MITM != nil && r.MITM.Enabled {
descriptions = append(descriptions, "mitm")
}
return descriptions
}
func (r *RuleActionRouteOptions) String() string {
return F.ToString("route-options(", strings.Join(r.Descriptions(), ","), ")")
return F.ToString("route-options(", strings.Join(descriptions, ","), ")")
}
type RuleActionDNSRoute struct {

View File

@@ -307,8 +307,10 @@ func (s *RemoteRuleSet) fetchOnce(ctx context.Context, startContext *adapter.HTT
func (s *RemoteRuleSet) Close() error {
s.rules = nil
s.updateTicker.Stop()
s.cancel()
if s.updateTicker != nil {
s.updateTicker.Stop()
}
return nil
}

View File

@@ -1,23 +0,0 @@
package jsc
import (
_ "unsafe"
"github.com/dop251/goja"
)
func NewUint8Array(runtime *goja.Runtime, data []byte) goja.Value {
buffer := runtime.NewArrayBuffer(data)
ctor, loaded := goja.AssertConstructor(runtimeGetUint8Array(runtime))
if !loaded {
panic(runtime.NewTypeError("missing UInt8Array constructor"))
}
array, err := ctor(nil, runtime.ToValue(buffer))
if err != nil {
panic(runtime.NewGoError(err))
}
return array
}
//go:linkname runtimeGetUint8Array github.com/dop251/goja.(*Runtime).getUint8Array
func runtimeGetUint8Array(r *goja.Runtime) *goja.Object

View File

@@ -1,18 +0,0 @@
package jsc_test
import (
"testing"
"github.com/sagernet/sing-box/script/jsc"
"github.com/dop251/goja"
"github.com/stretchr/testify/require"
)
func TestNewUInt8Array(t *testing.T) {
runtime := goja.New()
runtime.Set("hello", jsc.NewUint8Array(runtime, []byte("world")))
result, err := runtime.RunString("hello instanceof Uint8Array")
require.NoError(t, err)
require.True(t, result.ToBoolean())
}

View File

@@ -1,124 +0,0 @@
package jsc
import (
"net/http"
F "github.com/sagernet/sing/common/format"
"github.com/dop251/goja"
)
func IsNil(value goja.Value) bool {
return value == nil || goja.IsUndefined(value) || goja.IsNull(value)
}
func AssertObject(vm *goja.Runtime, value goja.Value, name string, nilable bool) *goja.Object {
if IsNil(value) {
if nilable {
return nil
}
panic(vm.NewTypeError(F.ToString("invalid argument: missing ", name)))
}
objectValue, isObject := value.(*goja.Object)
if !isObject {
panic(vm.NewTypeError(F.ToString("invalid argument: ", name, ": expected object, but got ", value)))
}
return objectValue
}
func AssertString(vm *goja.Runtime, value goja.Value, name string, nilable bool) string {
if IsNil(value) {
if nilable {
return ""
}
panic(vm.NewTypeError(F.ToString("invalid argument: missing ", name)))
}
stringValue, isString := value.Export().(string)
if !isString {
panic(vm.NewTypeError(F.ToString("invalid argument: ", name, ": expected string, but got ", value)))
}
return stringValue
}
func AssertInt(vm *goja.Runtime, value goja.Value, name string, nilable bool) int64 {
if IsNil(value) {
if nilable {
return 0
}
panic(vm.NewTypeError(F.ToString("invalid argument: missing ", name)))
}
integerValue, isNumber := value.Export().(int64)
if !isNumber {
panic(vm.NewTypeError(F.ToString("invalid argument: ", name, ": expected integer, but got ", value)))
}
return integerValue
}
func AssertBool(vm *goja.Runtime, value goja.Value, name string, nilable bool) bool {
if IsNil(value) {
if nilable {
return false
}
panic(vm.NewTypeError(F.ToString("invalid argument: missing ", name)))
}
boolValue, isBool := value.Export().(bool)
if !isBool {
panic(vm.NewTypeError(F.ToString("invalid argument: ", name, ": expected boolean, but got ", value)))
}
return boolValue
}
func AssertBinary(vm *goja.Runtime, value goja.Value, name string, nilable bool) []byte {
if IsNil(value) {
if nilable {
return nil
}
panic(vm.NewTypeError(F.ToString("invalid argument: missing ", name)))
}
switch exportedValue := value.Export().(type) {
case []byte:
return exportedValue
case goja.ArrayBuffer:
return exportedValue.Bytes()
default:
panic(vm.NewTypeError(F.ToString("invalid argument: ", name, ": expected Uint8Array or ArrayBuffer, but got ", value)))
}
}
func AssertStringBinary(vm *goja.Runtime, value goja.Value, name string, nilable bool) []byte {
if IsNil(value) {
if nilable {
return nil
}
panic(vm.NewTypeError(F.ToString("invalid argument: missing ", name)))
}
switch exportedValue := value.Export().(type) {
case string:
return []byte(exportedValue)
case []byte:
return exportedValue
case goja.ArrayBuffer:
return exportedValue.Bytes()
default:
panic(vm.NewTypeError(F.ToString("invalid argument: ", name, ": expected string, Uint8Array or ArrayBuffer, but got ", value)))
}
}
func AssertFunction(vm *goja.Runtime, value goja.Value, name string) goja.Callable {
if IsNil(value) {
panic(vm.NewTypeError(F.ToString("invalid argument: missing ", name)))
}
functionValue, isFunction := goja.AssertFunction(value)
if !isFunction {
panic(vm.NewTypeError(F.ToString("invalid argument: ", name, ": expected function, but got ", value)))
}
return functionValue
}
func AssertHTTPHeader(vm *goja.Runtime, value goja.Value, name string) http.Header {
headersObject := AssertObject(vm, value, name, true)
if headersObject == nil {
return nil
}
return ObjectToHeaders(vm, headersObject, name)
}

View File

@@ -1,192 +0,0 @@
package jsc
import (
"time"
"github.com/sagernet/sing/common"
"github.com/dop251/goja"
)
type Module interface {
Runtime() *goja.Runtime
}
type Class[M Module, C any] interface {
Module() M
Runtime() *goja.Runtime
DefineField(name string, getter func(this C) any, setter func(this C, value goja.Value))
DefineMethod(name string, method func(this C, call goja.FunctionCall) any)
DefineStaticMethod(name string, method func(c Class[M, C], call goja.FunctionCall) any)
DefineConstructor(constructor func(c Class[M, C], call goja.ConstructorCall) C)
ToValue() goja.Value
New(instance C) *goja.Object
Prototype() *goja.Object
Is(value goja.Value) bool
As(value goja.Value) C
}
func GetClass[M Module, C any](runtime *goja.Runtime, exports *goja.Object, className string) Class[M, C] {
objectValue := exports.Get(className)
if objectValue == nil {
panic(runtime.NewTypeError("Missing class: " + className))
}
object, isObject := objectValue.(*goja.Object)
if !isObject {
panic(runtime.NewTypeError("Invalid class: " + className))
}
classObject, isClass := object.Get("_class").(*goja.Object)
if !isClass {
panic(runtime.NewTypeError("Invalid class: " + className))
}
class, isClass := classObject.Export().(Class[M, C])
if !isClass {
panic(runtime.NewTypeError("Invalid class: " + className))
}
return class
}
type goClass[M Module, C any] struct {
m M
prototype *goja.Object
constructor goja.Value
}
func NewClass[M Module, C any](module M) Class[M, C] {
class := &goClass[M, C]{
m: module,
prototype: module.Runtime().NewObject(),
}
clazz := module.Runtime().ToValue(class).(*goja.Object)
clazz.Set("toString", module.Runtime().ToValue(func(call goja.FunctionCall) goja.Value {
return module.Runtime().ToValue("[sing-box Class]")
}))
class.prototype.DefineAccessorProperty("_class", class.Runtime().ToValue(func(call goja.FunctionCall) goja.Value { return clazz }), nil, goja.FLAG_FALSE, goja.FLAG_TRUE)
return class
}
func (c *goClass[M, C]) Module() M {
return c.m
}
func (c *goClass[M, C]) Runtime() *goja.Runtime {
return c.m.Runtime()
}
func (c *goClass[M, C]) DefineField(name string, getter func(this C) any, setter func(this C, value goja.Value)) {
var (
getterValue goja.Value
setterValue goja.Value
)
if getter != nil {
getterValue = c.Runtime().ToValue(func(call goja.FunctionCall) goja.Value {
this, isThis := call.This.Export().(C)
if !isThis {
panic(c.Runtime().NewTypeError("Illegal this value: " + call.This.ExportType().String()))
}
return c.toValue(getter(this), goja.Null())
})
}
if setter != nil {
setterValue = c.Runtime().ToValue(func(call goja.FunctionCall) goja.Value {
this, isThis := call.This.Export().(C)
if !isThis {
panic(c.Runtime().NewTypeError("Illegal this value: " + call.This.String()))
}
setter(this, call.Argument(0))
return goja.Undefined()
})
}
c.prototype.DefineAccessorProperty(name, getterValue, setterValue, goja.FLAG_FALSE, goja.FLAG_TRUE)
}
func (c *goClass[M, C]) DefineMethod(name string, method func(this C, call goja.FunctionCall) any) {
methodValue := c.Runtime().ToValue(func(call goja.FunctionCall) goja.Value {
this, isThis := call.This.Export().(C)
if !isThis {
panic(c.Runtime().NewTypeError("Illegal this value: " + call.This.String()))
}
return c.toValue(method(this, call), goja.Undefined())
})
c.prototype.Set(name, methodValue)
if name == "entries" {
c.prototype.DefineDataPropertySymbol(goja.SymIterator, methodValue, goja.FLAG_TRUE, goja.FLAG_FALSE, goja.FLAG_TRUE)
}
}
func (c *goClass[M, C]) DefineStaticMethod(name string, method func(c Class[M, C], call goja.FunctionCall) any) {
c.prototype.Set(name, c.Runtime().ToValue(func(call goja.FunctionCall) goja.Value {
return c.toValue(method(c, call), goja.Undefined())
}))
}
func (c *goClass[M, C]) DefineConstructor(constructor func(c Class[M, C], call goja.ConstructorCall) C) {
constructorObject := c.Runtime().ToValue(func(call goja.ConstructorCall) *goja.Object {
value := constructor(c, call)
object := c.toValue(value, goja.Undefined()).(*goja.Object)
object.SetPrototype(call.This.Prototype())
return object
}).(*goja.Object)
constructorObject.SetPrototype(c.prototype)
c.prototype.DefineDataProperty("constructor", constructorObject, goja.FLAG_FALSE, goja.FLAG_FALSE, goja.FLAG_FALSE)
c.constructor = constructorObject
}
func (c *goClass[M, C]) toValue(rawValue any, defaultValue goja.Value) goja.Value {
switch value := rawValue.(type) {
case nil:
return defaultValue
case time.Time:
return TimeToValue(c.Runtime(), value)
default:
return c.Runtime().ToValue(value)
}
}
func (c *goClass[M, C]) ToValue() goja.Value {
if c.constructor == nil {
constructorObject := c.Runtime().ToValue(func(call goja.ConstructorCall) *goja.Object {
panic(c.Runtime().NewTypeError("Illegal constructor call"))
}).(*goja.Object)
constructorObject.SetPrototype(c.prototype)
c.prototype.DefineDataProperty("constructor", constructorObject, goja.FLAG_FALSE, goja.FLAG_FALSE, goja.FLAG_FALSE)
c.constructor = constructorObject
}
return c.constructor
}
func (c *goClass[M, C]) New(instance C) *goja.Object {
object := c.Runtime().ToValue(instance).(*goja.Object)
object.SetPrototype(c.prototype)
return object
}
func (c *goClass[M, C]) Prototype() *goja.Object {
return c.prototype
}
func (c *goClass[M, C]) Is(value goja.Value) bool {
object, isObject := value.(*goja.Object)
if !isObject {
return false
}
prototype := object.Prototype()
for prototype != nil {
if prototype == c.prototype {
return true
}
prototype = prototype.Prototype()
}
return false
}
func (c *goClass[M, C]) As(value goja.Value) C {
object, isObject := value.(*goja.Object)
if !isObject {
return common.DefaultValue[C]()
}
if !c.Is(object) {
return common.DefaultValue[C]()
}
return object.Export().(C)
}

View File

@@ -1,56 +0,0 @@
package jsc
import (
"net/http"
"reflect"
"github.com/sagernet/sing/common"
F "github.com/sagernet/sing/common/format"
"github.com/dop251/goja"
)
func HeadersToValue(runtime *goja.Runtime, headers http.Header) goja.Value {
object := runtime.NewObject()
for key, value := range headers {
if len(value) == 1 {
object.Set(key, value[0])
} else {
object.Set(key, ArrayToValue(runtime, value))
}
}
return object
}
func ArrayToValue[T any](runtime *goja.Runtime, values []T) goja.Value {
return runtime.NewArray(common.Map(values, func(it T) any { return it })...)
}
func ObjectToHeaders(vm *goja.Runtime, object *goja.Object, name string) http.Header {
headers := make(http.Header)
for _, key := range object.Keys() {
valueObject := object.Get(key)
switch headerValue := valueObject.(type) {
case goja.String:
headers.Set(key, headerValue.String())
case *goja.Object:
values := headerValue.Export()
valueArray, isArray := values.([]any)
if !isArray {
panic(vm.NewTypeError(F.ToString("invalid value: ", name, ".", key, "expected string or string array, got ", valueObject.String())))
}
newValues := make([]string, 0, len(valueArray))
for _, value := range valueArray {
stringValue, isString := value.(string)
if !isString {
panic(vm.NewTypeError(F.ToString("invalid value: ", name, ".", key, " expected string or string array, got array item type: ", reflect.TypeOf(value))))
}
newValues = append(newValues, stringValue)
}
headers[key] = newValues
default:
panic(vm.NewTypeError(F.ToString("invalid value: ", name, ".", key, " expected string or string array, got ", valueObject.String())))
}
}
return headers
}

View File

@@ -1,31 +0,0 @@
package jsc_test
import (
"fmt"
"net/http"
"reflect"
"testing"
"github.com/sagernet/sing-box/script/jsc"
"github.com/dop251/goja"
"github.com/stretchr/testify/require"
)
func TestHeaders(t *testing.T) {
runtime := goja.New()
runtime.Set("headers", jsc.HeadersToValue(runtime, http.Header{
"My-Header": []string{"My-Value1", "My-Value2"},
}))
headers := runtime.Get("headers").(*goja.Object).Get("My-Header").(*goja.Object)
fmt.Println(reflect.ValueOf(headers.Export()).Type().String())
}
func TestBody(t *testing.T) {
runtime := goja.New()
_, err := runtime.RunString(`
var responseBody = new Uint8Array([1, 2, 3, 4, 5])
`)
require.NoError(t, err)
fmt.Println(reflect.TypeOf(runtime.Get("responseBody").Export()))
}

View File

@@ -1,36 +0,0 @@
package jsc
import "github.com/dop251/goja"
type Iterator[M Module, T any] struct {
c Class[M, *Iterator[M, T]]
values []T
block func(this T) any
}
func NewIterator[M Module, T any](class Class[M, *Iterator[M, T]], values []T, block func(this T) any) goja.Value {
return class.New(&Iterator[M, T]{class, values, block})
}
func CreateIterator[M Module, T any](module M) Class[M, *Iterator[M, T]] {
class := NewClass[M, *Iterator[M, T]](module)
class.DefineMethod("next", (*Iterator[M, T]).next)
class.DefineMethod("toString", (*Iterator[M, T]).toString)
return class
}
func (i *Iterator[M, T]) next(call goja.FunctionCall) any {
result := i.c.Runtime().NewObject()
if len(i.values) == 0 {
result.Set("done", true)
} else {
result.Set("done", false)
result.Set("value", i.block(i.values[0]))
i.values = i.values[1:]
}
return result
}
func (i *Iterator[M, T]) toString(call goja.FunctionCall) any {
return "[sing-box Iterator]"
}

View File

@@ -1,18 +0,0 @@
package jsc
import (
"time"
_ "unsafe"
"github.com/dop251/goja"
)
func TimeToValue(runtime *goja.Runtime, time time.Time) goja.Value {
return runtimeNewDateObject(runtime, time, true, runtimeGetDatePrototype(runtime))
}
//go:linkname runtimeNewDateObject github.com/dop251/goja.(*Runtime).newDateObject
func runtimeNewDateObject(r *goja.Runtime, t time.Time, isSet bool, proto *goja.Object) *goja.Object
//go:linkname runtimeGetDatePrototype github.com/dop251/goja.(*Runtime).getDatePrototype
func runtimeGetDatePrototype(r *goja.Runtime) *goja.Object

View File

@@ -1,20 +0,0 @@
package jsc_test
import (
"testing"
"time"
"github.com/sagernet/sing-box/script/jsc"
"github.com/dop251/goja"
"github.com/stretchr/testify/require"
)
func TestTimeToValue(t *testing.T) {
t.Parallel()
runtime := goja.New()
now := time.Now()
err := runtime.Set("now", jsc.TimeToValue(runtime, now))
require.NoError(t, err)
println(runtime.Get("now").String())
}

View File

@@ -1,83 +0,0 @@
'use strict';
const assert = {
_isSameValue(a, b) {
if (a === b) {
// Handle +/-0 vs. -/+0
return a !== 0 || 1 / a === 1 / b;
}
// Handle NaN vs. NaN
return a !== a && b !== b;
},
_toString(value) {
try {
if (value === 0 && 1 / value === -Infinity) {
return '-0';
}
return String(value);
} catch (err) {
if (err.name === 'TypeError') {
return Object.prototype.toString.call(value);
}
throw err;
}
},
sameValue(actual, expected, message) {
if (assert._isSameValue(actual, expected)) {
return;
}
if (message === undefined) {
message = '';
} else {
message += ' ';
}
message += 'Expected SameValue(«' + assert._toString(actual) + '», «' + assert._toString(expected) + '») to be true';
throw new Error(message);
},
throws(f, ctor, message) {
if (message === undefined) {
message = '';
} else {
message += ' ';
}
try {
f();
} catch (e) {
if (e.constructor !== ctor) {
throw new Error(message + "Wrong exception type was thrown: " + e.constructor.name);
}
return;
}
throw new Error(message + "No exception was thrown");
},
throwsNodeError(f, ctor, code, message) {
if (message === undefined) {
message = '';
} else {
message += ' ';
}
try {
f();
} catch (e) {
if (e.constructor !== ctor) {
throw new Error(message + "Wrong exception type was thrown: " + e.constructor.name);
}
if (e.code !== code) {
throw new Error(message + "Wrong exception code was thrown: " + e.code);
}
return;
}
throw new Error(message + "No exception was thrown");
}
}
module.exports = assert;

View File

@@ -1,21 +0,0 @@
package jstest
import (
_ "embed"
"github.com/sagernet/sing-box/script/modules/require"
)
//go:embed assert.js
var assertJS []byte
func NewRegistry() *require.Registry {
return require.NewRegistry(require.WithFsEnable(true), require.WithLoader(func(path string) ([]byte, error) {
switch path {
case "assert.js":
return assertJS, nil
default:
return require.DefaultSourceLoader(path)
}
}))
}

View File

@@ -1,116 +0,0 @@
package script
import (
"context"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/taskmonitor"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/common/logger"
"github.com/sagernet/sing/common/task"
)
var _ adapter.ScriptManager = (*Manager)(nil)
type Manager struct {
ctx context.Context
logger logger.ContextLogger
scripts []adapter.Script
scriptByName map[string]adapter.Script
surgeCache *adapter.SurgeInMemoryCache
}
func NewManager(ctx context.Context, logFactory log.Factory, scripts []option.Script) (*Manager, error) {
manager := &Manager{
ctx: ctx,
logger: logFactory.NewLogger("script"),
scriptByName: make(map[string]adapter.Script),
}
for _, scriptOptions := range scripts {
script, err := NewScript(ctx, logFactory.NewLogger(F.ToString("script/", scriptOptions.Type, "[", scriptOptions.Tag, "]")), scriptOptions)
if err != nil {
return nil, E.Cause(err, "initialize script: ", scriptOptions.Tag)
}
manager.scripts = append(manager.scripts, script)
manager.scriptByName[scriptOptions.Tag] = script
}
return manager, nil
}
func (m *Manager) Start(stage adapter.StartStage) error {
monitor := taskmonitor.New(m.logger, C.StartTimeout)
switch stage {
case adapter.StartStateStart:
var cacheContext *adapter.HTTPStartContext
if len(m.scripts) > 0 {
monitor.Start("initialize rule-set")
cacheContext = adapter.NewHTTPStartContext(m.ctx)
var scriptStartGroup task.Group
for _, script := range m.scripts {
scriptInPlace := script
scriptStartGroup.Append0(func(ctx context.Context) error {
err := scriptInPlace.StartContext(ctx, cacheContext)
if err != nil {
return E.Cause(err, "initialize script/", scriptInPlace.Type(), "[", scriptInPlace.Tag(), "]")
}
return nil
})
}
scriptStartGroup.Concurrency(5)
scriptStartGroup.FastFail()
err := scriptStartGroup.Run(m.ctx)
monitor.Finish()
if err != nil {
return err
}
}
if cacheContext != nil {
cacheContext.Close()
}
case adapter.StartStatePostStart:
for _, script := range m.scripts {
monitor.Start(F.ToString("post start script/", script.Type(), "[", script.Tag(), "]"))
err := script.PostStart()
monitor.Finish()
if err != nil {
return E.Cause(err, "post start script/", script.Type(), "[", script.Tag(), "]")
}
}
}
return nil
}
func (m *Manager) Close() error {
monitor := taskmonitor.New(m.logger, C.StopTimeout)
var err error
for _, script := range m.scripts {
monitor.Start(F.ToString("close start script/", script.Type(), "[", script.Tag(), "]"))
err = E.Append(err, script.Close(), func(err error) error {
return E.Cause(err, "close script/", script.Type(), "[", script.Tag(), "]")
})
monitor.Finish()
}
return err
}
func (m *Manager) Scripts() []adapter.Script {
return m.scripts
}
func (m *Manager) Script(name string) (adapter.Script, bool) {
script, loaded := m.scriptByName[name]
return script, loaded
}
func (m *Manager) SurgeCache() *adapter.SurgeInMemoryCache {
if m.surgeCache == nil {
m.surgeCache = &adapter.SurgeInMemoryCache{
Data: make(map[string]string),
}
}
return m.surgeCache
}

View File

@@ -1,50 +0,0 @@
package boxctx
import (
"context"
"time"
"github.com/sagernet/sing-box/script/jsc"
"github.com/sagernet/sing/common/logger"
"github.com/dop251/goja"
)
type Context struct {
class jsc.Class[*Module, *Context]
Context context.Context
Logger logger.ContextLogger
Tag string
StartedAt time.Time
ErrorHandler func(error)
}
func FromRuntime(runtime *goja.Runtime) *Context {
contextValue := runtime.Get("context")
if contextValue == nil {
return nil
}
context, isContext := contextValue.Export().(*Context)
if !isContext {
return nil
}
return context
}
func MustFromRuntime(runtime *goja.Runtime) *Context {
context := FromRuntime(runtime)
if context == nil {
panic(runtime.NewTypeError("Missing sing-box context"))
}
return context
}
func createContext(module *Module) jsc.Class[*Module, *Context] {
class := jsc.NewClass[*Module, *Context](module)
class.DefineMethod("toString", (*Context).toString)
return class
}
func (c *Context) toString(call goja.FunctionCall) any {
return "[sing-box Context]"
}

View File

@@ -1,35 +0,0 @@
package boxctx
import (
"github.com/sagernet/sing-box/script/jsc"
"github.com/sagernet/sing-box/script/modules/require"
"github.com/dop251/goja"
)
const ModuleName = "context"
type Module struct {
runtime *goja.Runtime
classContext jsc.Class[*Module, *Context]
}
func Require(runtime *goja.Runtime, module *goja.Object) {
m := &Module{
runtime: runtime,
}
m.classContext = createContext(m)
exports := module.Get("exports").(*goja.Object)
exports.Set("Context", m.classContext.ToValue())
}
func Enable(runtime *goja.Runtime, context *Context) {
exports := require.Require(runtime, ModuleName).ToObject(runtime)
classContext := jsc.GetClass[*Module, *Context](runtime, exports, "Context")
context.class = classContext
runtime.Set("context", classContext.New(context))
}
func (m *Module) Runtime() *goja.Runtime {
return m.runtime
}

View File

@@ -1,281 +0,0 @@
package console
import (
"bytes"
"context"
"encoding/xml"
"sync"
"time"
sLog "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/script/jsc"
"github.com/sagernet/sing-box/script/modules/boxctx"
F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/common/logger"
"github.com/dop251/goja"
)
type Console struct {
class jsc.Class[*Module, *Console]
access sync.Mutex
countMap map[string]int
timeMap map[string]time.Time
}
func NewConsole(class jsc.Class[*Module, *Console]) goja.Value {
return class.New(&Console{
class: class,
countMap: make(map[string]int),
timeMap: make(map[string]time.Time),
})
}
func createConsole(m *Module) jsc.Class[*Module, *Console] {
class := jsc.NewClass[*Module, *Console](m)
class.DefineMethod("assert", (*Console).assert)
class.DefineMethod("clear", (*Console).clear)
class.DefineMethod("count", (*Console).count)
class.DefineMethod("countReset", (*Console).countReset)
class.DefineMethod("debug", (*Console).debug)
class.DefineMethod("dir", (*Console).dir)
class.DefineMethod("dirxml", (*Console).dirxml)
class.DefineMethod("error", (*Console).error)
class.DefineMethod("group", (*Console).stub)
class.DefineMethod("groupCollapsed", (*Console).stub)
class.DefineMethod("groupEnd", (*Console).stub)
class.DefineMethod("info", (*Console).info)
class.DefineMethod("log", (*Console)._log)
class.DefineMethod("profile", (*Console).stub)
class.DefineMethod("profileEnd", (*Console).profileEnd)
class.DefineMethod("table", (*Console).table)
class.DefineMethod("time", (*Console).time)
class.DefineMethod("timeEnd", (*Console).timeEnd)
class.DefineMethod("timeLog", (*Console).timeLog)
class.DefineMethod("timeStamp", (*Console).stub)
class.DefineMethod("trace", (*Console).trace)
class.DefineMethod("warn", (*Console).warn)
return class
}
func (c *Console) stub(call goja.FunctionCall) any {
return goja.Undefined()
}
func (c *Console) assert(call goja.FunctionCall) any {
assertion := call.Argument(0).ToBoolean()
if !assertion {
return c.log(logger.ContextLogger.ErrorContext, call.Arguments[1:])
}
return goja.Undefined()
}
func (c *Console) clear(call goja.FunctionCall) any {
return nil
}
func (c *Console) count(call goja.FunctionCall) any {
label := jsc.AssertString(c.class.Runtime(), call.Argument(0), "label", true)
if label == "" {
label = "default"
}
c.access.Lock()
newValue := c.countMap[label] + 1
c.countMap[label] = newValue
c.access.Unlock()
writeLog(c.class.Runtime(), logger.ContextLogger.InfoContext, F.ToString(label, ": ", newValue))
return goja.Undefined()
}
func (c *Console) countReset(call goja.FunctionCall) any {
label := jsc.AssertString(c.class.Runtime(), call.Argument(0), "label", true)
if label == "" {
label = "default"
}
c.access.Lock()
delete(c.countMap, label)
c.access.Unlock()
return goja.Undefined()
}
func (c *Console) log(logFunc func(logger.ContextLogger, context.Context, ...any), args []goja.Value) any {
var buffer bytes.Buffer
var formatString string
if len(args) > 0 {
formatString = args[0].String()
}
format(c.class.Runtime(), &buffer, formatString, args[1:]...)
writeLog(c.class.Runtime(), logFunc, buffer.String())
return goja.Undefined()
}
func (c *Console) debug(call goja.FunctionCall) any {
return c.log(logger.ContextLogger.DebugContext, call.Arguments)
}
func (c *Console) dir(call goja.FunctionCall) any {
object := jsc.AssertObject(c.class.Runtime(), call.Argument(0), "object", false)
var buffer bytes.Buffer
for _, key := range object.Keys() {
value := object.Get(key)
buffer.WriteString(key)
buffer.WriteString(": ")
buffer.WriteString(value.String())
buffer.WriteString("\n")
}
writeLog(c.class.Runtime(), logger.ContextLogger.InfoContext, buffer.String())
return goja.Undefined()
}
func (c *Console) dirxml(call goja.FunctionCall) any {
var buffer bytes.Buffer
encoder := xml.NewEncoder(&buffer)
encoder.Indent("", " ")
encoder.Encode(call.Argument(0).Export())
writeLog(c.class.Runtime(), logger.ContextLogger.InfoContext, buffer.String())
return goja.Undefined()
}
func (c *Console) error(call goja.FunctionCall) any {
return c.log(logger.ContextLogger.ErrorContext, call.Arguments)
}
func (c *Console) info(call goja.FunctionCall) any {
return c.log(logger.ContextLogger.InfoContext, call.Arguments)
}
func (c *Console) _log(call goja.FunctionCall) any {
return c.log(logger.ContextLogger.InfoContext, call.Arguments)
}
func (c *Console) profileEnd(call goja.FunctionCall) any {
return goja.Undefined()
}
func (c *Console) table(call goja.FunctionCall) any {
return c.dir(call)
}
func (c *Console) time(call goja.FunctionCall) any {
label := jsc.AssertString(c.class.Runtime(), call.Argument(0), "label", true)
if label == "" {
label = "default"
}
c.access.Lock()
c.timeMap[label] = time.Now()
c.access.Unlock()
return goja.Undefined()
}
func (c *Console) timeEnd(call goja.FunctionCall) any {
label := jsc.AssertString(c.class.Runtime(), call.Argument(0), "label", true)
if label == "" {
label = "default"
}
c.access.Lock()
startTime, ok := c.timeMap[label]
if !ok {
c.access.Unlock()
return goja.Undefined()
}
delete(c.timeMap, label)
c.access.Unlock()
writeLog(c.class.Runtime(), logger.ContextLogger.InfoContext, F.ToString(label, ": ", time.Since(startTime).String(), " - - timer ended"))
return goja.Undefined()
}
func (c *Console) timeLog(call goja.FunctionCall) any {
label := jsc.AssertString(c.class.Runtime(), call.Argument(0), "label", true)
if label == "" {
label = "default"
}
c.access.Lock()
startTime, ok := c.timeMap[label]
c.access.Unlock()
if !ok {
writeLog(c.class.Runtime(), logger.ContextLogger.ErrorContext, F.ToString("Timer \"", label, "\" doesn't exist."))
return goja.Undefined()
}
writeLog(c.class.Runtime(), logger.ContextLogger.InfoContext, F.ToString(label, ": ", time.Since(startTime)))
return goja.Undefined()
}
func (c *Console) trace(call goja.FunctionCall) any {
return c.log(logger.ContextLogger.TraceContext, call.Arguments)
}
func (c *Console) warn(call goja.FunctionCall) any {
return c.log(logger.ContextLogger.WarnContext, call.Arguments)
}
func writeLog(runtime *goja.Runtime, logFunc func(logger.ContextLogger, context.Context, ...any), message string) {
var (
ctx context.Context
sLogger logger.ContextLogger
)
boxCtx := boxctx.FromRuntime(runtime)
if boxCtx != nil {
ctx = boxCtx.Context
sLogger = boxCtx.Logger
} else {
ctx = context.Background()
sLogger = sLog.StdLogger()
}
logFunc(sLogger, ctx, message)
}
func format(runtime *goja.Runtime, b *bytes.Buffer, f string, args ...goja.Value) {
pct := false
argNum := 0
for _, chr := range f {
if pct {
if argNum < len(args) {
if format1(runtime, chr, args[argNum], b) {
argNum++
}
} else {
b.WriteByte('%')
b.WriteRune(chr)
}
pct = false
} else {
if chr == '%' {
pct = true
} else {
b.WriteRune(chr)
}
}
}
for _, arg := range args[argNum:] {
b.WriteByte(' ')
b.WriteString(arg.String())
}
}
func format1(runtime *goja.Runtime, f rune, val goja.Value, w *bytes.Buffer) bool {
switch f {
case 's':
w.WriteString(val.String())
case 'd':
w.WriteString(val.ToNumber().String())
case 'j':
if json, ok := runtime.Get("JSON").(*goja.Object); ok {
if stringify, ok := goja.AssertFunction(json.Get("stringify")); ok {
res, err := stringify(json, val)
if err != nil {
panic(err)
}
w.WriteString(res.String())
}
}
case '%':
w.WriteByte('%')
return false
default:
w.WriteByte('%')
w.WriteRune(f)
return false
}
return true
}

View File

@@ -1,3 +0,0 @@
package console
type Context struct{}

View File

@@ -1,34 +0,0 @@
package console
import (
"github.com/sagernet/sing-box/script/jsc"
"github.com/sagernet/sing-box/script/modules/require"
"github.com/dop251/goja"
)
const ModuleName = "console"
type Module struct {
runtime *goja.Runtime
console jsc.Class[*Module, *Console]
}
func Require(runtime *goja.Runtime, module *goja.Object) {
m := &Module{
runtime: runtime,
}
m.console = createConsole(m)
exports := module.Get("exports").(*goja.Object)
exports.Set("Console", m.console.ToValue())
}
func Enable(runtime *goja.Runtime) {
exports := require.Require(runtime, ModuleName).ToObject(runtime)
classConsole := jsc.GetClass[*Module, *Console](runtime, exports, "Console")
runtime.Set("console", NewConsole(classConsole))
}
func (m *Module) Runtime() *goja.Runtime {
return m.runtime
}

View File

@@ -1,489 +0,0 @@
package eventloop
import (
"sync"
"sync/atomic"
"time"
"github.com/dop251/goja"
)
type job struct {
cancel func() bool
fn func()
idx int
cancelled bool
}
type Timer struct {
job
timer *time.Timer
}
type Interval struct {
job
ticker *time.Ticker
stopChan chan struct{}
}
type Immediate struct {
job
}
type EventLoop struct {
vm *goja.Runtime
jobChan chan func()
jobs []*job
jobCount int32
canRun int32
auxJobsLock sync.Mutex
wakeupChan chan struct{}
auxJobsSpare, auxJobs []func()
stopLock sync.Mutex
stopCond *sync.Cond
running bool
terminated bool
errorHandler func(error)
}
func Enable(runtime *goja.Runtime, errorHandler func(error)) *EventLoop {
loop := &EventLoop{
vm: runtime,
jobChan: make(chan func()),
wakeupChan: make(chan struct{}, 1),
errorHandler: errorHandler,
}
loop.stopCond = sync.NewCond(&loop.stopLock)
runtime.Set("setTimeout", loop.setTimeout)
runtime.Set("setInterval", loop.setInterval)
runtime.Set("setImmediate", loop.setImmediate)
runtime.Set("clearTimeout", loop.clearTimeout)
runtime.Set("clearInterval", loop.clearInterval)
runtime.Set("clearImmediate", loop.clearImmediate)
return loop
}
func (loop *EventLoop) schedule(call goja.FunctionCall, repeating bool) goja.Value {
if fn, ok := goja.AssertFunction(call.Argument(0)); ok {
delay := call.Argument(1).ToInteger()
var args []goja.Value
if len(call.Arguments) > 2 {
args = append(args, call.Arguments[2:]...)
}
f := func() {
_, err := fn(nil, args...)
if err != nil {
loop.errorHandler(err)
}
}
loop.jobCount++
var job *job
var ret goja.Value
if repeating {
interval := loop.newInterval(f)
interval.start(loop, time.Duration(delay)*time.Millisecond)
job = &interval.job
ret = loop.vm.ToValue(interval)
} else {
timeout := loop.newTimeout(f)
timeout.start(loop, time.Duration(delay)*time.Millisecond)
job = &timeout.job
ret = loop.vm.ToValue(timeout)
}
job.idx = len(loop.jobs)
loop.jobs = append(loop.jobs, job)
return ret
}
return nil
}
func (loop *EventLoop) setTimeout(call goja.FunctionCall) goja.Value {
return loop.schedule(call, false)
}
func (loop *EventLoop) setInterval(call goja.FunctionCall) goja.Value {
return loop.schedule(call, true)
}
func (loop *EventLoop) setImmediate(call goja.FunctionCall) goja.Value {
if fn, ok := goja.AssertFunction(call.Argument(0)); ok {
var args []goja.Value
if len(call.Arguments) > 1 {
args = append(args, call.Arguments[1:]...)
}
f := func() {
_, err := fn(nil, args...)
if err != nil {
loop.errorHandler(err)
}
}
loop.jobCount++
return loop.vm.ToValue(loop.addImmediate(f))
}
return nil
}
// SetTimeout schedules to run the specified function in the context
// of the loop as soon as possible after the specified timeout period.
// SetTimeout returns a Timer which can be passed to ClearTimeout.
// The instance of goja.Runtime that is passed to the function and any Values derived
// from it must not be used outside the function. SetTimeout is
// safe to call inside or outside the loop.
// If the loop is terminated (see Terminate()) returns nil.
func (loop *EventLoop) SetTimeout(fn func(*goja.Runtime), timeout time.Duration) *Timer {
t := loop.newTimeout(func() { fn(loop.vm) })
if loop.addAuxJob(func() {
t.start(loop, timeout)
loop.jobCount++
t.idx = len(loop.jobs)
loop.jobs = append(loop.jobs, &t.job)
}) {
return t
}
return nil
}
// ClearTimeout cancels a Timer returned by SetTimeout if it has not run yet.
// ClearTimeout is safe to call inside or outside the loop.
func (loop *EventLoop) ClearTimeout(t *Timer) {
loop.addAuxJob(func() {
loop.clearTimeout(t)
})
}
// SetInterval schedules to repeatedly run the specified function in
// the context of the loop as soon as possible after every specified
// timeout period. SetInterval returns an Interval which can be
// passed to ClearInterval. The instance of goja.Runtime that is passed to the
// function and any Values derived from it must not be used outside
// the function. SetInterval is safe to call inside or outside the
// loop.
// If the loop is terminated (see Terminate()) returns nil.
func (loop *EventLoop) SetInterval(fn func(*goja.Runtime), timeout time.Duration) *Interval {
i := loop.newInterval(func() { fn(loop.vm) })
if loop.addAuxJob(func() {
i.start(loop, timeout)
loop.jobCount++
i.idx = len(loop.jobs)
loop.jobs = append(loop.jobs, &i.job)
}) {
return i
}
return nil
}
// ClearInterval cancels an Interval returned by SetInterval.
// ClearInterval is safe to call inside or outside the loop.
func (loop *EventLoop) ClearInterval(i *Interval) {
loop.addAuxJob(func() {
loop.clearInterval(i)
})
}
func (loop *EventLoop) setRunning() {
loop.stopLock.Lock()
defer loop.stopLock.Unlock()
if loop.running {
panic("Loop is already started")
}
loop.running = true
atomic.StoreInt32(&loop.canRun, 1)
loop.auxJobsLock.Lock()
loop.terminated = false
loop.auxJobsLock.Unlock()
}
// Run calls the specified function, starts the event loop and waits until there are no more delayed jobs to run
// after which it stops the loop and returns.
// The instance of goja.Runtime that is passed to the function and any Values derived from it must not be used
// outside the function.
// Do NOT use this function while the loop is already running. Use RunOnLoop() instead.
// If the loop is already started it will panic.
func (loop *EventLoop) Run(fn func(*goja.Runtime)) {
loop.setRunning()
fn(loop.vm)
loop.run(false)
}
// Start the event loop in the background. The loop continues to run until Stop() is called.
// If the loop is already started it will panic.
func (loop *EventLoop) Start() {
loop.setRunning()
go loop.run(true)
}
// StartInForeground starts the event loop in the current goroutine. The loop continues to run until Stop() is called.
// If the loop is already started it will panic.
// Use this instead of Start if you want to recover from panics that may occur while calling native Go functions from
// within setInterval and setTimeout callbacks.
func (loop *EventLoop) StartInForeground() {
loop.setRunning()
loop.run(true)
}
// Stop the loop that was started with Start(). After this function returns there will be no more jobs executed
// by the loop. It is possible to call Start() or Run() again after this to resume the execution.
// Note, it does not cancel active timeouts (use Terminate() instead if you want this).
// It is not allowed to run Start() (or Run()) and Stop() or Terminate() concurrently.
// Calling Stop() on a non-running loop has no effect.
// It is not allowed to call Stop() from the loop, because it is synchronous and cannot complete until the loop
// is not running any jobs. Use StopNoWait() instead.
// return number of jobs remaining
func (loop *EventLoop) Stop() int {
loop.stopLock.Lock()
for loop.running {
atomic.StoreInt32(&loop.canRun, 0)
loop.wakeup()
loop.stopCond.Wait()
}
loop.stopLock.Unlock()
return int(loop.jobCount)
}
// StopNoWait tells the loop to stop and returns immediately. Can be used inside the loop. Calling it on a
// non-running loop has no effect.
func (loop *EventLoop) StopNoWait() {
loop.stopLock.Lock()
if loop.running {
atomic.StoreInt32(&loop.canRun, 0)
loop.wakeup()
}
loop.stopLock.Unlock()
}
// Terminate stops the loop and clears all active timeouts and intervals. After it returns there are no
// active timers or goroutines associated with the loop. Any attempt to submit a task (by using RunOnLoop(),
// SetTimeout() or SetInterval()) will not succeed.
// After being terminated the loop can be restarted again by using Start() or Run().
// This method must not be called concurrently with Stop*(), Start(), or Run().
func (loop *EventLoop) Terminate() {
loop.Stop()
loop.auxJobsLock.Lock()
loop.terminated = true
loop.auxJobsLock.Unlock()
loop.runAux()
for i := 0; i < len(loop.jobs); i++ {
job := loop.jobs[i]
if !job.cancelled {
job.cancelled = true
if job.cancel() {
loop.removeJob(job)
i--
}
}
}
for len(loop.jobs) > 0 {
(<-loop.jobChan)()
}
}
// RunOnLoop schedules to run the specified function in the context of the loop as soon as possible.
// The order of the runs is preserved (i.e. the functions will be called in the same order as calls to RunOnLoop())
// The instance of goja.Runtime that is passed to the function and any Values derived from it must not be used
// outside the function. It is safe to call inside or outside the loop.
// Returns true on success or false if the loop is terminated (see Terminate()).
func (loop *EventLoop) RunOnLoop(fn func(*goja.Runtime)) bool {
return loop.addAuxJob(func() { fn(loop.vm) })
}
func (loop *EventLoop) runAux() {
loop.auxJobsLock.Lock()
jobs := loop.auxJobs
loop.auxJobs = loop.auxJobsSpare
loop.auxJobsLock.Unlock()
for i, job := range jobs {
job()
jobs[i] = nil
}
loop.auxJobsSpare = jobs[:0]
}
func (loop *EventLoop) run(inBackground bool) {
loop.runAux()
if inBackground {
loop.jobCount++
}
LOOP:
for loop.jobCount > 0 {
select {
case job := <-loop.jobChan:
job()
case <-loop.wakeupChan:
loop.runAux()
if atomic.LoadInt32(&loop.canRun) == 0 {
break LOOP
}
}
}
if inBackground {
loop.jobCount--
}
loop.stopLock.Lock()
loop.running = false
loop.stopLock.Unlock()
loop.stopCond.Broadcast()
}
func (loop *EventLoop) wakeup() {
select {
case loop.wakeupChan <- struct{}{}:
default:
}
}
func (loop *EventLoop) addAuxJob(fn func()) bool {
loop.auxJobsLock.Lock()
if loop.terminated {
loop.auxJobsLock.Unlock()
return false
}
loop.auxJobs = append(loop.auxJobs, fn)
loop.auxJobsLock.Unlock()
loop.wakeup()
return true
}
func (loop *EventLoop) newTimeout(f func()) *Timer {
t := &Timer{
job: job{fn: f},
}
t.cancel = t.doCancel
return t
}
func (t *Timer) start(loop *EventLoop, timeout time.Duration) {
t.timer = time.AfterFunc(timeout, func() {
loop.jobChan <- func() {
loop.doTimeout(t)
}
})
}
func (loop *EventLoop) newInterval(f func()) *Interval {
i := &Interval{
job: job{fn: f},
stopChan: make(chan struct{}),
}
i.cancel = i.doCancel
return i
}
func (i *Interval) start(loop *EventLoop, timeout time.Duration) {
// https://nodejs.org/api/timers.html#timers_setinterval_callback_delay_args
if timeout <= 0 {
timeout = time.Millisecond
}
i.ticker = time.NewTicker(timeout)
go i.run(loop)
}
func (loop *EventLoop) addImmediate(f func()) *Immediate {
i := &Immediate{
job: job{fn: f},
}
loop.addAuxJob(func() {
loop.doImmediate(i)
})
return i
}
func (loop *EventLoop) doTimeout(t *Timer) {
loop.removeJob(&t.job)
if !t.cancelled {
t.cancelled = true
loop.jobCount--
t.fn()
}
}
func (loop *EventLoop) doInterval(i *Interval) {
if !i.cancelled {
i.fn()
}
}
func (loop *EventLoop) doImmediate(i *Immediate) {
if !i.cancelled {
i.cancelled = true
loop.jobCount--
i.fn()
}
}
func (loop *EventLoop) clearTimeout(t *Timer) {
if t != nil && !t.cancelled {
t.cancelled = true
loop.jobCount--
if t.doCancel() {
loop.removeJob(&t.job)
}
}
}
func (loop *EventLoop) clearInterval(i *Interval) {
if i != nil && !i.cancelled {
i.cancelled = true
loop.jobCount--
i.doCancel()
}
}
func (loop *EventLoop) removeJob(job *job) {
idx := job.idx
if idx < 0 {
return
}
if idx < len(loop.jobs)-1 {
loop.jobs[idx] = loop.jobs[len(loop.jobs)-1]
loop.jobs[idx].idx = idx
}
loop.jobs[len(loop.jobs)-1] = nil
loop.jobs = loop.jobs[:len(loop.jobs)-1]
job.idx = -1
}
func (loop *EventLoop) clearImmediate(i *Immediate) {
if i != nil && !i.cancelled {
i.cancelled = true
loop.jobCount--
}
}
func (i *Interval) doCancel() bool {
close(i.stopChan)
return false
}
func (t *Timer) doCancel() bool {
return t.timer.Stop()
}
func (i *Interval) run(loop *EventLoop) {
L:
for {
select {
case <-i.stopChan:
i.ticker.Stop()
break L
case <-i.ticker.C:
loop.jobChan <- func() {
loop.doInterval(i)
}
}
}
loop.jobChan <- func() {
loop.removeJob(&i.job)
}
}

View File

@@ -1,231 +0,0 @@
package require
import (
"errors"
"io"
"io/fs"
"os"
"path"
"path/filepath"
"runtime"
"sync"
"syscall"
"text/template"
js "github.com/dop251/goja"
"github.com/dop251/goja/parser"
)
type ModuleLoader func(*js.Runtime, *js.Object)
// SourceLoader represents a function that returns a file data at a given path.
// The function should return ModuleFileDoesNotExistError if the file either doesn't exist or is a directory.
// This error will be ignored by the resolver and the search will continue. Any other errors will be propagated.
type SourceLoader func(path string) ([]byte, error)
var (
InvalidModuleError = errors.New("Invalid module")
IllegalModuleNameError = errors.New("Illegal module name")
NoSuchBuiltInModuleError = errors.New("No such built-in module")
ModuleFileDoesNotExistError = errors.New("module file does not exist")
)
// Registry contains a cache of compiled modules which can be used by multiple Runtimes
type Registry struct {
sync.Mutex
native map[string]ModuleLoader
builtin map[string]ModuleLoader
compiled map[string]*js.Program
srcLoader SourceLoader
globalFolders []string
fsEnabled bool
}
type RequireModule struct {
r *Registry
runtime *js.Runtime
modules map[string]*js.Object
nodeModules map[string]*js.Object
}
func NewRegistry(opts ...Option) *Registry {
r := &Registry{}
for _, opt := range opts {
opt(r)
}
return r
}
type Option func(*Registry)
// WithLoader sets a function which will be called by the require() function in order to get a source code for a
// module at the given path. The same function will be used to get external source maps.
// Note, this only affects the modules loaded by the require() function. If you need to use it as a source map
// loader for code parsed in a different way (such as runtime.RunString() or eval()), use (*Runtime).SetParserOptions()
func WithLoader(srcLoader SourceLoader) Option {
return func(r *Registry) {
r.srcLoader = srcLoader
}
}
// WithGlobalFolders appends the given paths to the registry's list of
// global folders to search if the requested module is not found
// elsewhere. By default, a registry's global folders list is empty.
// In the reference Node.js implementation, the default global folders
// list is $NODE_PATH, $HOME/.node_modules, $HOME/.node_libraries and
// $PREFIX/lib/node, see
// https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders.
func WithGlobalFolders(globalFolders ...string) Option {
return func(r *Registry) {
r.globalFolders = globalFolders
}
}
func WithFsEnable(enabled bool) Option {
return func(r *Registry) {
r.fsEnabled = enabled
}
}
// Enable adds the require() function to the specified runtime.
func (r *Registry) Enable(runtime *js.Runtime) *RequireModule {
rrt := &RequireModule{
r: r,
runtime: runtime,
modules: make(map[string]*js.Object),
nodeModules: make(map[string]*js.Object),
}
runtime.Set("require", rrt.require)
return rrt
}
func (r *Registry) RegisterNodeModule(name string, loader ModuleLoader) {
r.Lock()
defer r.Unlock()
if r.builtin == nil {
r.builtin = make(map[string]ModuleLoader)
}
name = filepathClean(name)
r.builtin[name] = loader
}
func (r *Registry) RegisterNativeModule(name string, loader ModuleLoader) {
r.Lock()
defer r.Unlock()
if r.native == nil {
r.native = make(map[string]ModuleLoader)
}
name = filepathClean(name)
r.native[name] = loader
}
// DefaultSourceLoader is used if none was set (see WithLoader()). It simply loads files from the host's filesystem.
func DefaultSourceLoader(filename string) ([]byte, error) {
fp := filepath.FromSlash(filename)
f, err := os.Open(fp)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
err = ModuleFileDoesNotExistError
} else if runtime.GOOS == "windows" {
if errors.Is(err, syscall.Errno(0x7b)) { // ERROR_INVALID_NAME, The filename, directory name, or volume label syntax is incorrect.
err = ModuleFileDoesNotExistError
}
}
return nil, err
}
defer f.Close()
// On some systems (e.g. plan9 and FreeBSD) it is possible to use the standard read() call on directories
// which means we cannot rely on read() returning an error, we have to do stat() instead.
if fi, err := f.Stat(); err == nil {
if fi.IsDir() {
return nil, ModuleFileDoesNotExistError
}
} else {
return nil, err
}
return io.ReadAll(f)
}
func (r *Registry) getSource(p string) ([]byte, error) {
srcLoader := r.srcLoader
if srcLoader == nil {
srcLoader = DefaultSourceLoader
}
return srcLoader(p)
}
func (r *Registry) getCompiledSource(p string) (*js.Program, error) {
r.Lock()
defer r.Unlock()
prg := r.compiled[p]
if prg == nil {
buf, err := r.getSource(p)
if err != nil {
return nil, err
}
s := string(buf)
if path.Ext(p) == ".json" {
s = "module.exports = JSON.parse('" + template.JSEscapeString(s) + "')"
}
source := "(function(exports, require, module) {" + s + "\n})"
parsed, err := js.Parse(p, source, parser.WithSourceMapLoader(r.srcLoader))
if err != nil {
return nil, err
}
prg, err = js.CompileAST(parsed, false)
if err == nil {
if r.compiled == nil {
r.compiled = make(map[string]*js.Program)
}
r.compiled[p] = prg
}
return prg, err
}
return prg, nil
}
func (r *RequireModule) require(call js.FunctionCall) js.Value {
ret, err := r.Require(call.Argument(0).String())
if err != nil {
if _, ok := err.(*js.Exception); !ok {
panic(r.runtime.NewGoError(err))
}
panic(err)
}
return ret
}
func filepathClean(p string) string {
return path.Clean(p)
}
// Require can be used to import modules from Go source (similar to JS require() function).
func (r *RequireModule) Require(p string) (ret js.Value, err error) {
module, err := r.resolve(p)
if err != nil {
return
}
ret = module.Get("exports")
return
}
func Require(runtime *js.Runtime, name string) js.Value {
if r, ok := js.AssertFunction(runtime.Get("require")); ok {
mod, err := r(js.Undefined(), runtime.ToValue(name))
if err != nil {
panic(err)
}
return mod
}
panic(runtime.NewTypeError("Please enable require for this runtime using new(require.Registry).Enable(runtime)"))
}

View File

@@ -1,277 +0,0 @@
package require
import (
"encoding/json"
"errors"
"path"
"path/filepath"
"runtime"
"strings"
js "github.com/dop251/goja"
)
const NodePrefix = "node:"
// NodeJS module search algorithm described by
// https://nodejs.org/api/modules.html#modules_all_together
func (r *RequireModule) resolve(modpath string) (module *js.Object, err error) {
origPath, modpath := modpath, filepathClean(modpath)
if modpath == "" {
return nil, IllegalModuleNameError
}
var start string
err = nil
if path.IsAbs(origPath) {
start = "/"
} else {
start = r.getCurrentModulePath()
}
p := path.Join(start, modpath)
if isFileOrDirectoryPath(origPath) && r.r.fsEnabled {
if module = r.modules[p]; module != nil {
return
}
module, err = r.loadAsFileOrDirectory(p)
if err == nil && module != nil {
r.modules[p] = module
}
} else {
module, err = r.loadNative(origPath)
if err == nil {
return
} else {
if err == InvalidModuleError {
err = nil
} else {
return
}
}
if module = r.nodeModules[p]; module != nil {
return
}
if r.r.fsEnabled {
module, err = r.loadNodeModules(modpath, start)
if err == nil && module != nil {
r.nodeModules[p] = module
}
}
}
if module == nil && err == nil {
err = InvalidModuleError
}
return
}
func (r *RequireModule) loadNative(path string) (*js.Object, error) {
module := r.modules[path]
if module != nil {
return module, nil
}
var ldr ModuleLoader
if r.r.native != nil {
ldr = r.r.native[path]
}
var isBuiltIn, withPrefix bool
if ldr == nil {
if r.r.builtin != nil {
ldr = r.r.builtin[path]
}
if ldr == nil && strings.HasPrefix(path, NodePrefix) {
ldr = r.r.builtin[path[len(NodePrefix):]]
if ldr == nil {
return nil, NoSuchBuiltInModuleError
}
withPrefix = true
}
isBuiltIn = true
}
if ldr != nil {
module = r.createModuleObject()
r.modules[path] = module
if isBuiltIn {
if withPrefix {
r.modules[path[len(NodePrefix):]] = module
} else {
if !strings.HasPrefix(path, NodePrefix) {
r.modules[NodePrefix+path] = module
}
}
}
ldr(r.runtime, module)
return module, nil
}
return nil, InvalidModuleError
}
func (r *RequireModule) loadAsFileOrDirectory(path string) (module *js.Object, err error) {
if module, err = r.loadAsFile(path); module != nil || err != nil {
return
}
return r.loadAsDirectory(path)
}
func (r *RequireModule) loadAsFile(path string) (module *js.Object, err error) {
if module, err = r.loadModule(path); module != nil || err != nil {
return
}
p := path + ".js"
if module, err = r.loadModule(p); module != nil || err != nil {
return
}
p = path + ".json"
return r.loadModule(p)
}
func (r *RequireModule) loadIndex(modpath string) (module *js.Object, err error) {
p := path.Join(modpath, "index.js")
if module, err = r.loadModule(p); module != nil || err != nil {
return
}
p = path.Join(modpath, "index.json")
return r.loadModule(p)
}
func (r *RequireModule) loadAsDirectory(modpath string) (module *js.Object, err error) {
p := path.Join(modpath, "package.json")
buf, err := r.r.getSource(p)
if err != nil {
return r.loadIndex(modpath)
}
var pkg struct {
Main string
}
err = json.Unmarshal(buf, &pkg)
if err != nil || len(pkg.Main) == 0 {
return r.loadIndex(modpath)
}
m := path.Join(modpath, pkg.Main)
if module, err = r.loadAsFile(m); module != nil || err != nil {
return
}
return r.loadIndex(m)
}
func (r *RequireModule) loadNodeModule(modpath, start string) (*js.Object, error) {
return r.loadAsFileOrDirectory(path.Join(start, modpath))
}
func (r *RequireModule) loadNodeModules(modpath, start string) (module *js.Object, err error) {
for _, dir := range r.r.globalFolders {
if module, err = r.loadNodeModule(modpath, dir); module != nil || err != nil {
return
}
}
for {
var p string
if path.Base(start) != "node_modules" {
p = path.Join(start, "node_modules")
} else {
p = start
}
if module, err = r.loadNodeModule(modpath, p); module != nil || err != nil {
return
}
if start == ".." { // Dir('..') is '.'
break
}
parent := path.Dir(start)
if parent == start {
break
}
start = parent
}
return
}
func (r *RequireModule) getCurrentModulePath() string {
var buf [2]js.StackFrame
frames := r.runtime.CaptureCallStack(2, buf[:0])
if len(frames) < 2 {
return "."
}
return path.Dir(frames[1].SrcName())
}
func (r *RequireModule) createModuleObject() *js.Object {
module := r.runtime.NewObject()
module.Set("exports", r.runtime.NewObject())
return module
}
func (r *RequireModule) loadModule(path string) (*js.Object, error) {
module := r.modules[path]
if module == nil {
module = r.createModuleObject()
r.modules[path] = module
err := r.loadModuleFile(path, module)
if err != nil {
module = nil
delete(r.modules, path)
if errors.Is(err, ModuleFileDoesNotExistError) {
err = nil
}
}
return module, err
}
return module, nil
}
func (r *RequireModule) loadModuleFile(path string, jsModule *js.Object) error {
prg, err := r.r.getCompiledSource(path)
if err != nil {
return err
}
f, err := r.runtime.RunProgram(prg)
if err != nil {
return err
}
if call, ok := js.AssertFunction(f); ok {
jsExports := jsModule.Get("exports")
jsRequire := r.runtime.Get("require")
// Run the module source, with "jsExports" as "this",
// "jsExports" as the "exports" variable, "jsRequire"
// as the "require" variable and "jsModule" as the
// "module" variable (Nodejs capable).
_, err = call(jsExports, jsExports, jsRequire, jsModule)
if err != nil {
return err
}
} else {
return InvalidModuleError
}
return nil
}
func isFileOrDirectoryPath(path string) bool {
result := path == "." || path == ".." ||
strings.HasPrefix(path, "/") ||
strings.HasPrefix(path, "./") ||
strings.HasPrefix(path, "../")
if runtime.GOOS == "windows" {
result = result ||
strings.HasPrefix(path, `.\`) ||
strings.HasPrefix(path, `..\`) ||
filepath.IsAbs(path)
}
return result
}

View File

@@ -1,111 +0,0 @@
package sgnotification
import (
"context"
"encoding/base64"
"strings"
"github.com/sagernet/sing-box/experimental/libbox/platform"
"github.com/sagernet/sing-box/script/jsc"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
"github.com/sagernet/sing/service"
"github.com/dop251/goja"
)
type SurgeNotification struct {
vm *goja.Runtime
logger logger.Logger
platformInterface platform.Interface
scriptTag string
}
func Enable(vm *goja.Runtime, ctx context.Context, logger logger.Logger) {
platformInterface := service.FromContext[platform.Interface](ctx)
notification := &SurgeNotification{
vm: vm,
logger: logger,
platformInterface: platformInterface,
}
notificationObject := vm.NewObject()
notificationObject.Set("post", notification.js_post)
vm.Set("$notification", notificationObject)
}
func (s *SurgeNotification) js_post(call goja.FunctionCall) goja.Value {
var (
title string
subtitle string
body string
openURL string
clipboard string
mediaURL string
mediaData []byte
mediaType string
autoDismiss int
)
title = jsc.AssertString(s.vm, call.Argument(0), "title", true)
subtitle = jsc.AssertString(s.vm, call.Argument(1), "subtitle", true)
body = jsc.AssertString(s.vm, call.Argument(2), "body", true)
options := jsc.AssertObject(s.vm, call.Argument(3), "options", true)
if options != nil {
action := jsc.AssertString(s.vm, options.Get("action"), "options.action", true)
switch action {
case "open-url":
openURL = jsc.AssertString(s.vm, options.Get("url"), "options.url", false)
case "clipboard":
clipboard = jsc.AssertString(s.vm, options.Get("clipboard"), "options.clipboard", false)
}
mediaURL = jsc.AssertString(s.vm, options.Get("media-url"), "options.media-url", true)
mediaBase64 := jsc.AssertString(s.vm, options.Get("media-base64"), "options.media-base64", true)
if mediaBase64 != "" {
mediaBinary, err := base64.StdEncoding.DecodeString(mediaBase64)
if err != nil {
panic(s.vm.NewGoError(E.Cause(err, "decode media-base64")))
}
mediaData = mediaBinary
mediaType = jsc.AssertString(s.vm, options.Get("media-base64-mime"), "options.media-base64-mime", false)
}
autoDismiss = int(jsc.AssertInt(s.vm, options.Get("auto-dismiss"), "options.auto-dismiss", true))
}
if title != "" && subtitle == "" && body == "" {
body = title
title = ""
} else if title != "" && subtitle != "" && body == "" {
body = subtitle
subtitle = ""
}
var builder strings.Builder
if title != "" {
builder.WriteString("[")
builder.WriteString(title)
if subtitle != "" {
builder.WriteString(" - ")
builder.WriteString(subtitle)
}
builder.WriteString("]: ")
}
builder.WriteString(body)
s.logger.Info("notification: " + builder.String())
if s.platformInterface != nil {
err := s.platformInterface.SendNotification(&platform.Notification{
Identifier: "surge-script-notification-" + s.scriptTag,
TypeName: "Surge Script Notification (" + s.scriptTag + ")",
TypeID: 11,
Title: title,
Subtitle: subtitle,
Body: body,
OpenURL: openURL,
Clipboard: clipboard,
MediaURL: mediaURL,
MediaData: mediaData,
MediaType: mediaType,
Timeout: autoDismiss,
})
if err != nil {
s.logger.Error(E.Cause(err, "send notification"))
}
}
return goja.Undefined()
}

View File

@@ -1,65 +0,0 @@
package surge
import (
"runtime"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/experimental/locale"
"github.com/sagernet/sing-box/script/jsc"
"github.com/dop251/goja"
)
type Environment struct {
class jsc.Class[*Module, *Environment]
}
func createEnvironment(module *Module) jsc.Class[*Module, *Environment] {
class := jsc.NewClass[*Module, *Environment](module)
class.DefineField("system", (*Environment).getSystem, nil)
class.DefineField("surge-build", (*Environment).getSurgeBuild, nil)
class.DefineField("surge-version", (*Environment).getSurgeVersion, nil)
class.DefineField("language", (*Environment).getLanguage, nil)
class.DefineField("device-model", (*Environment).getDeviceModel, nil)
class.DefineMethod("toString", (*Environment).toString)
return class
}
func (e *Environment) getSystem() any {
switch runtime.GOOS {
case "ios":
return "iOS"
case "darwin":
return "macOS"
case "tvos":
return "tvOS"
case "linux":
return "Linux"
case "android":
return "Android"
case "windows":
return "Windows"
default:
return runtime.GOOS
}
}
func (e *Environment) getSurgeBuild() any {
return "N/A"
}
func (e *Environment) getSurgeVersion() any {
return "sing-box " + C.Version
}
func (e *Environment) getLanguage() any {
return locale.Current().Locale
}
func (e *Environment) getDeviceModel() any {
return "N/A"
}
func (e *Environment) toString(call goja.FunctionCall) any {
return "[sing-box Surge environment"
}

View File

@@ -1,150 +0,0 @@
package surge
import (
"bytes"
"crypto/tls"
"io"
"net/http"
"net/http/cookiejar"
"time"
"github.com/sagernet/sing-box/script/jsc"
"github.com/sagernet/sing-box/script/modules/boxctx"
"github.com/sagernet/sing/common"
F "github.com/sagernet/sing/common/format"
"github.com/dop251/goja"
"golang.org/x/net/publicsuffix"
)
type HTTP struct {
class jsc.Class[*Module, *HTTP]
cookieJar *cookiejar.Jar
httpTransport *http.Transport
}
func createHTTP(module *Module) jsc.Class[*Module, *HTTP] {
class := jsc.NewClass[*Module, *HTTP](module)
class.DefineConstructor(newHTTP)
class.DefineMethod("get", httpRequest(http.MethodGet))
class.DefineMethod("post", httpRequest(http.MethodPost))
class.DefineMethod("put", httpRequest(http.MethodPut))
class.DefineMethod("delete", httpRequest(http.MethodDelete))
class.DefineMethod("head", httpRequest(http.MethodHead))
class.DefineMethod("options", httpRequest(http.MethodOptions))
class.DefineMethod("patch", httpRequest(http.MethodPatch))
class.DefineMethod("trace", httpRequest(http.MethodTrace))
class.DefineMethod("toString", (*HTTP).toString)
return class
}
func newHTTP(class jsc.Class[*Module, *HTTP], call goja.ConstructorCall) *HTTP {
return &HTTP{
class: class,
cookieJar: common.Must1(cookiejar.New(&cookiejar.Options{
PublicSuffixList: publicsuffix.List,
})),
httpTransport: &http.Transport{
ForceAttemptHTTP2: true,
TLSClientConfig: &tls.Config{},
},
}
}
func httpRequest(method string) func(s *HTTP, call goja.FunctionCall) any {
return func(s *HTTP, call goja.FunctionCall) any {
if len(call.Arguments) != 2 {
panic(s.class.Runtime().NewTypeError("invalid arguments"))
}
context := boxctx.MustFromRuntime(s.class.Runtime())
var (
url string
headers http.Header
body []byte
timeout = 5 * time.Second
insecure bool
autoCookie bool = true
autoRedirect bool
// policy string
binaryMode bool
)
switch optionsValue := call.Argument(0).(type) {
case goja.String:
url = optionsValue.String()
case *goja.Object:
url = jsc.AssertString(s.class.Runtime(), optionsValue.Get("url"), "options.url", false)
headers = jsc.AssertHTTPHeader(s.class.Runtime(), optionsValue.Get("headers"), "option.headers")
body = jsc.AssertStringBinary(s.class.Runtime(), optionsValue.Get("body"), "options.body", true)
timeoutInt := jsc.AssertInt(s.class.Runtime(), optionsValue.Get("timeout"), "options.timeout", true)
if timeoutInt > 0 {
timeout = time.Duration(timeoutInt) * time.Second
}
insecure = jsc.AssertBool(s.class.Runtime(), optionsValue.Get("insecure"), "options.insecure", true)
autoCookie = jsc.AssertBool(s.class.Runtime(), optionsValue.Get("auto-cookie"), "options.auto-cookie", true)
autoRedirect = jsc.AssertBool(s.class.Runtime(), optionsValue.Get("auto-redirect"), "options.auto-redirect", true)
// policy = jsc.AssertString(s.class.Runtime(), optionsValue.Get("policy"), "options.policy", true)
binaryMode = jsc.AssertBool(s.class.Runtime(), optionsValue.Get("binary-mode"), "options.binary-mode", true)
default:
panic(s.class.Runtime().NewTypeError(F.ToString("invalid argument: options: expected string or object, but got ", optionsValue)))
}
callback := jsc.AssertFunction(s.class.Runtime(), call.Argument(1), "callback")
s.httpTransport.TLSClientConfig.InsecureSkipVerify = insecure
httpClient := &http.Client{
Timeout: timeout,
Transport: s.httpTransport,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
if autoRedirect {
return nil
}
return http.ErrUseLastResponse
},
}
if autoCookie {
httpClient.Jar = s.cookieJar
}
request, err := http.NewRequestWithContext(context.Context, method, url, bytes.NewReader(body))
if host := headers.Get("Host"); host != "" {
request.Host = host
headers.Del("Host")
}
request.Header = headers
if err != nil {
panic(s.class.Runtime().NewGoError(err))
}
go func() {
defer s.httpTransport.CloseIdleConnections()
response, executeErr := httpClient.Do(request)
if err != nil {
_, err = callback(nil, s.class.Runtime().NewGoError(executeErr), nil, nil)
if err != nil {
context.ErrorHandler(err)
}
return
}
defer response.Body.Close()
var content []byte
content, err = io.ReadAll(response.Body)
if err != nil {
_, err = callback(nil, s.class.Runtime().NewGoError(err), nil, nil)
if err != nil {
context.ErrorHandler(err)
}
}
responseObject := s.class.Runtime().NewObject()
responseObject.Set("status", response.StatusCode)
responseObject.Set("headers", jsc.HeadersToValue(s.class.Runtime(), response.Header))
var bodyValue goja.Value
if binaryMode {
bodyValue = jsc.NewUint8Array(s.class.Runtime(), content)
} else {
bodyValue = s.class.Runtime().ToValue(string(content))
}
_, err = callback(nil, nil, responseObject, bodyValue)
}()
return nil
}
}
func (h *HTTP) toString(call goja.FunctionCall) any {
return "[sing-box Surge HTTP]"
}

View File

@@ -1,63 +0,0 @@
package surge
import (
"github.com/sagernet/sing-box/script/jsc"
"github.com/sagernet/sing-box/script/modules/require"
"github.com/sagernet/sing/common"
"github.com/dop251/goja"
)
const ModuleName = "surge"
type Module struct {
runtime *goja.Runtime
classScript jsc.Class[*Module, *Script]
classEnvironment jsc.Class[*Module, *Environment]
classPersistentStore jsc.Class[*Module, *PersistentStore]
classHTTP jsc.Class[*Module, *HTTP]
classUtils jsc.Class[*Module, *Utils]
classNotification jsc.Class[*Module, *Notification]
}
func Require(runtime *goja.Runtime, module *goja.Object) {
m := &Module{
runtime: runtime,
}
m.classScript = createScript(m)
m.classEnvironment = createEnvironment(m)
m.classPersistentStore = createPersistentStore(m)
m.classHTTP = createHTTP(m)
m.classUtils = createUtils(m)
m.classNotification = createNotification(m)
exports := module.Get("exports").(*goja.Object)
exports.Set("Script", m.classScript.ToValue())
exports.Set("Environment", m.classEnvironment.ToValue())
exports.Set("PersistentStore", m.classPersistentStore.ToValue())
exports.Set("HTTP", m.classHTTP.ToValue())
exports.Set("Utils", m.classUtils.ToValue())
exports.Set("Notification", m.classNotification.ToValue())
}
func Enable(runtime *goja.Runtime, scriptType string, args []string) {
exports := require.Require(runtime, ModuleName).ToObject(runtime)
classScript := jsc.GetClass[*Module, *Script](runtime, exports, "Script")
classEnvironment := jsc.GetClass[*Module, *Environment](runtime, exports, "Environment")
classPersistentStore := jsc.GetClass[*Module, *PersistentStore](runtime, exports, "PersistentStore")
classHTTP := jsc.GetClass[*Module, *HTTP](runtime, exports, "HTTP")
classUtils := jsc.GetClass[*Module, *Utils](runtime, exports, "Utils")
classNotification := jsc.GetClass[*Module, *Notification](runtime, exports, "Notification")
runtime.Set("$script", classScript.New(&Script{class: classScript, ScriptType: scriptType}))
runtime.Set("$environment", classEnvironment.New(&Environment{class: classEnvironment}))
runtime.Set("$persistentStore", newPersistentStore(classPersistentStore))
runtime.Set("$http", classHTTP.New(newHTTP(classHTTP, goja.ConstructorCall{})))
runtime.Set("$utils", classUtils.New(&Utils{class: classUtils}))
runtime.Set("$notification", newNotification(classNotification))
runtime.Set("$argument", runtime.NewArray(common.Map(args, func(it string) any {
return it
})...))
}
func (m *Module) Runtime() *goja.Runtime {
return m.runtime
}

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