Compare commits

..

96 Commits

Author SHA1 Message Date
世界
847c80a1fe Fix 2025-04-14 23:40:54 +08:00
世界
40be0627f5 Add ssm-api service 2025-04-14 17:11:03 +08:00
世界
ae3b943b93 Add resolved service and DNS server 2025-04-14 15:42:07 +08:00
世界
7da9b40c66 Add DERP service 2025-04-14 15:42:05 +08:00
世界
48772992fa Add service component type 2025-04-13 16:24:46 +08:00
世界
a66772fa14 documentation: Bump version 2025-04-13 16:23:52 +08:00
世界
3b49ed8a71 Improve local DNS server 2025-04-13 16:23:52 +08:00
anytls
1c34ee26dc Update anytls
Co-authored-by: anytls <anytls>
2025-04-12 14:45:34 +08:00
世界
0ca4d8f327 Fix DNS dialer 2025-04-12 14:45:33 +08:00
世界
7e69892694 release: Skip override version for iOS 2025-04-12 14:45:33 +08:00
iikira
0494541057 Fix UDP DNS server crash
Signed-off-by: iikira <i2@mail.iikira.com>
2025-04-12 14:45:33 +08:00
ReleTor
b55d5914e0 Fix fetch ECH configs 2025-04-12 14:45:33 +08:00
世界
584fd40d59 release: Update Go to 1.24.2 2025-04-12 14:45:33 +08:00
世界
3c595f205b Allow direct outbounds without domain_resolver 2025-04-12 14:45:32 +08:00
世界
f75bd13383 Fix Tailscale dialer 2025-04-12 14:45:32 +08:00
dyhkwong
99effc44f8 Fix DNS over QUIC stream close 2025-04-12 14:45:32 +08:00
anytls
7abe17cea0 Update anytls
Co-authored-by: anytls <anytls>
2025-04-12 14:45:32 +08:00
Rambling2076
04f0ae6c00 Fix missing with_tailscale in Dockerfile
Signed-off-by: Rambling2076 <Rambling2076@proton.me>
2025-04-12 14:45:32 +08:00
世界
8f79ee6919 Fail when default DNS server not found 2025-04-12 14:45:31 +08:00
世界
29150fc0f7 Update gVisor to 20250319.0 2025-04-12 14:45:31 +08:00
世界
30ac4ca62c release: Do not build tailscale on iOS and tvOS 2025-04-12 14:45:30 +08:00
世界
819f14eca0 Explicitly reject detour to empty direct outbounds 2025-04-12 14:45:30 +08:00
世界
24df55660d Add netns support 2025-04-12 14:45:30 +08:00
世界
02943cefb3 Add wildcard name support for predefined records 2025-04-12 14:45:30 +08:00
世界
9f5a403532 Remove map usage in options 2025-04-12 14:45:29 +08:00
世界
9bcab743fc Fix unhandled DNS loop 2025-04-12 14:45:29 +08:00
世界
af47fc2543 Add wildcard-sni support for shadow-tls inbound 2025-04-12 14:45:29 +08:00
世界
07160909d7 Fix Tailscale DNS 2025-04-12 14:44:20 +08:00
k9982874
74c37a6f9c Add ntp protocol sniffing 2025-04-12 14:44:20 +08:00
世界
0dd11223ef option: Fix marshal legacy DNS options 2025-04-12 14:44:20 +08:00
世界
f635cdd3ef Make domain_resolver optional when only one DNS server is configured 2025-04-12 14:44:19 +08:00
世界
dccf5c56fb Fix DNS lookup context pollution 2025-04-12 14:44:19 +08:00
世界
12141bb74a Fix http3 DNS server connecting to wrong address 2025-04-12 14:44:19 +08:00
Restia-Ashbell
657bd4725f documentation: Fix typo 2025-04-12 14:44:18 +08:00
anytls
c50c8815f0 Update sing-anytls
Co-authored-by: anytls <anytls>
2025-04-12 14:44:18 +08:00
k9982874
3026b37734 Fix hosts DNS server 2025-04-12 14:44:18 +08:00
世界
ecbd8f55f6 Fix UDP DNS server crash 2025-04-12 14:44:18 +08:00
世界
743b00f748 documentation: Fix missing ip_accept_any DNS rule option 2025-04-12 14:44:17 +08:00
世界
0249fc5e67 Fix anytls dialer usage 2025-04-12 14:44:17 +08:00
世界
51e63cbeba Move predefined DNS server to rule action 2025-04-12 14:44:17 +08:00
世界
b6a9e0ce0c Fix domain resolver on direct outbound 2025-04-12 14:44:16 +08:00
Zephyruso
3114db2450 Fix missing AnyTLS display name 2025-04-12 14:44:16 +08:00
anytls
ac29d4ca9c Update sing-anytls
Co-authored-by: anytls <anytls>
2025-04-12 14:44:16 +08:00
Estel
70bb012989 documentation: Fix typo
Signed-off-by: Estel <callmebedrockdigger@gmail.com>
2025-04-12 14:44:16 +08:00
TargetLocked
c6090a32dc Fix parsing legacy DNS options 2025-04-12 14:44:15 +08:00
世界
e7d0227c90 Fix DNS fallback 2025-04-12 14:44:15 +08:00
世界
6bbab76cc7 documentation: Fix missing hosts DNS server 2025-04-12 14:44:14 +08:00
anytls
0af1236d8f Add MinIdleSession option to AnyTLS outbound
Co-authored-by: anytls <anytls>
2025-04-12 14:44:14 +08:00
ReleTor
488e26aebc documentation: Minor fixes 2025-04-12 14:44:14 +08:00
libtry486
8a2a65133a documentation: Fix typo
fix typo

Signed-off-by: libtry486 <89328481+libtry486@users.noreply.github.com>
2025-04-12 14:44:14 +08:00
Alireza Ahmadi
5e6cc89c65 Fix Outbound deadlock 2025-04-12 14:44:14 +08:00
世界
3145df4b6c documentation: Fix AnyTLS doc 2025-04-12 14:44:13 +08:00
anytls
4b2f455e1e Add AnyTLS protocol 2025-04-12 14:44:12 +08:00
世界
02619e3dfa Migrate to stdlib ECH support 2025-04-12 14:44:12 +08:00
世界
00082ab6c8 Add fallback local DNS server for iOS 2025-04-12 14:44:12 +08:00
世界
ce26077d83 Get darwin local DNS server from libresolv 2025-04-12 14:44:11 +08:00
世界
089e13451b Improve resolve action 2025-04-12 14:44:11 +08:00
世界
d3184a7997 Fix toolchain version 2025-04-12 14:44:11 +08:00
世界
65f5767180 Add back port hopping to hysteria 1 2025-04-12 14:44:10 +08:00
世界
c594f33dc4 Update dependencies 2025-04-12 14:44:10 +08:00
xchacha20-poly1305
e65a9d7380 Remove single quotes of raw Moziila certs 2025-04-12 14:44:10 +08:00
世界
dd304bebfc Add Tailscale endpoint 2025-04-12 14:44:09 +08:00
世界
6a6c6a5ba5 Build legacy binaries with latest Go 2025-04-12 14:44:09 +08:00
世界
b99dc4609d documentation: Remove outdated icons 2025-04-12 14:44:08 +08:00
世界
e59dab657e documentation: Certificate store 2025-04-12 14:44:08 +08:00
世界
908174ddf6 documentation: TLS fragment 2025-04-12 14:44:08 +08:00
世界
34c48fa421 documentation: Outbound domain resolver 2025-04-12 14:44:07 +08:00
世界
1fe5b8fd9d documentation: Refactor DNS 2025-04-12 14:44:07 +08:00
世界
4bef6ea0e8 Add certificate store 2025-04-12 14:44:07 +08:00
世界
941049d21b Add TLS fragment support 2025-04-12 14:44:07 +08:00
世界
bdc1724975 refactor: Outbound domain resolver 2025-04-12 14:44:06 +08:00
世界
942511ad6d refactor: DNS 2025-04-12 14:44:06 +08:00
世界
cb68a40c43 documentation: Update actual behaviors of auto_redirect and strict_route 2025-04-12 13:06:16 +08:00
纳西妲 · Nahida
023218e6e7 Fix build will fail when use space to split each tag 2025-04-12 13:06:16 +08:00
世界
2a24b94b8d Minor fixes 2025-04-12 13:06:15 +08:00
世界
c6531cf184 Fix NTP service 2025-04-12 13:06:15 +08:00
世界
d4fa0ed349 Improve auto redirect 2025-04-12 13:06:10 +08:00
世界
10874d2dc4 Bump version 2025-04-08 14:34:09 +08:00
Fei1Yang
5adaf1ac75 Mark config file as noreplace for rpm 2025-04-08 14:21:08 +08:00
世界
9668ea69b8 Fix windows process searcher 2025-04-08 14:16:27 +08:00
testing
ae9bc7acf1 documentation: Fix typo
Signed-off-by: testing <58134720+testing765@users.noreply.github.com>
2025-04-08 14:16:23 +08:00
世界
594ee480a2 option: Fix listable 2025-04-08 14:16:23 +08:00
世界
a15b5a2463 Fix no_drop not work 2025-04-08 14:16:23 +08:00
Mahdi
991e755789 Fix conn copy 2025-04-08 14:16:22 +08:00
世界
97d41ffde8 Improve pause management 2025-04-08 14:16:22 +08:00
世界
24af0766ac Fix uTP sniffer 2025-04-08 14:16:22 +08:00
世界
af17eaa537 Improve sniffer 2025-04-08 14:16:22 +08:00
世界
3adc10a797 Fix hysteria2 close 2025-04-08 14:16:22 +08:00
xchacha20-poly1305
5eeef6b28e Fix multiple trackers 2025-04-08 14:16:22 +08:00
世界
f4c29840c3 Fix DNS sniffer 2025-03-31 20:45:04 +08:00
世界
47fc3ebda4 Add duplicate tag check 2025-03-29 23:10:22 +08:00
世界
9774a659b0 Fix DoQ / truncate DNS message 2025-03-29 17:41:22 +08:00
世界
2e4a6de4e7 release: Fix read tag 2025-03-27 20:30:57 +08:00
世界
a530e424e9 Bump version 2025-03-27 18:17:39 +08:00
世界
0bfd487ee9 Fix udpnat2 handler again 2025-03-27 18:17:39 +08:00
世界
6aae834493 release: Fix workflow 2025-03-27 18:17:39 +08:00
132 changed files with 3999 additions and 2292 deletions

4
.fpm
View File

@@ -6,11 +6,15 @@
--url "https://sing-box.sagernet.org/"
--maintainer "nekohasekai <contact-git@sekai.icu>"
--deb-field "Bug: https://github.com/SagerNet/sing-box/issues"
--config-files /etc/sing-box/config.json
release/config/config.json=/etc/sing-box/config.json
release/config/sing-box.service=/usr/lib/systemd/system/sing-box.service
release/config/sing-box@.service=/usr/lib/systemd/system/sing-box@.service
release/config/sing-box.sysusers=/usr/lib/sysusers.d/sing-box.conf
release/config/sing-box.rules=usr/share/polkit-1/rules.d/sing-box.rules
release/config/sing-box-split-dns.xml=/usr/share/dbus-1/system.d/sing-box-split-dns.conf
release/completions/sing-box.bash=/usr/share/bash-completion/completions/sing-box.bash
release/completions/sing-box.fish=/usr/share/fish/vendor_completions.d/sing-box.fish

View File

@@ -46,7 +46,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.24
go-version: ^1.24.2
- name: Check input version
if: github.event_name == 'workflow_dispatch'
run: |-
@@ -102,7 +102,7 @@ jobs:
if: ${{ ! matrix.legacy_go }}
uses: actions/setup-go@v5
with:
go-version: ^1.24
go-version: ^1.24.2
- name: Cache Legacy Go
if: matrix.require_legacy_go
id: cache-legacy-go
@@ -133,10 +133,7 @@ jobs:
- name: Set build tags
run: |
set -xeuo pipefail
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_reality_server,with_acme,with_clash_api'
if [ ! '${{ matrix.legacy_go }}' = 'true' ]; then
TAGS="${TAGS},with_ech"
fi
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_reality_server,with_acme,with_clash_api,with_tailscale'
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
- name: Build
if: matrix.os != 'android'
@@ -176,6 +173,9 @@ jobs:
PKG_NAME="sing-box_${{ needs.calculate_version.outputs.version }}_${{ matrix.os }}_${{ matrix.arch }}${ARM_VERSION}"
echo "DIR_NAME=${DIR_NAME}" >> "${GITHUB_ENV}"
echo "PKG_NAME=${PKG_NAME}" >> "${GITHUB_ENV}"
PKG_VERSION="${{ needs.calculate_version.outputs.version }}"
PKG_VERSION="${PKG_VERSION//-/\~}"
echo "PKG_VERSION=${PKG_VERSION}" >> "${GITHUB_ENV}"
- name: Package DEB
if: matrix.debian != ''
run: |
@@ -183,7 +183,7 @@ jobs:
sudo gem install fpm
sudo apt-get install -y debsigs
fpm -t deb \
-v "${{ needs.calculate_version.outputs.version }}" \
-v "$PKG_VERSION" \
-p "dist/${PKG_NAME}.deb" \
--architecture ${{ matrix.debian }} \
dist/sing-box=/usr/bin/sing-box
@@ -200,7 +200,7 @@ jobs:
set -xeuo pipefail
sudo gem install fpm
fpm -t rpm \
-v "${{ needs.calculate_version.outputs.version }}" \
-v "$PKG_VERSION" \
-p "dist/${PKG_NAME}.rpm" \
--architecture ${{ matrix.rpm }} \
dist/sing-box=/usr/bin/sing-box
@@ -219,7 +219,7 @@ jobs:
sudo gem install fpm
sudo apt-get install -y libarchive-tools
fpm -t pacman \
-v "${{ needs.calculate_version.outputs.version }}" \
-v "$PKG_VERSION" \
-p "dist/${PKG_NAME}.pkg.tar.zst" \
--architecture ${{ matrix.pacman }} \
dist/sing-box=/usr/bin/sing-box
@@ -259,7 +259,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.24
go-version: ^1.24.2
- name: Setup Android NDK
id: setup-ndk
uses: nttld/setup-ndk@v1
@@ -339,7 +339,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.24
go-version: ^1.24.2
- name: Setup Android NDK
id: setup-ndk
uses: nttld/setup-ndk@v1
@@ -437,7 +437,7 @@ jobs:
if: matrix.if
uses: actions/setup-go@v5
with:
go-version: ^1.24
go-version: ^1.24.2
- name: Setup Xcode stable
if: matrix.if && github.ref == 'refs/heads/main-next'
run: |-
@@ -514,10 +514,13 @@ jobs:
MACOS_PROJECT_VERSION=$(go run -v ./cmd/internal/app_store_connect next_macos_project_version)
echo "MACOS_PROJECT_VERSION=$MACOS_PROJECT_VERSION"
echo "MACOS_PROJECT_VERSION=$MACOS_PROJECT_VERSION" >> "$GITHUB_ENV"
- name: Update version
if: matrix.if && matrix.name != 'iOS'
run: |-
go run -v ./cmd/internal/update_apple_version --ci
- name: Build
if: matrix.if
run: |-
go run -v ./cmd/internal/update_apple_version --ci
cd clients/apple
xcodebuild archive \
-scheme "${{ matrix.scheme }}" \

View File

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

View File

@@ -25,7 +25,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.24
go-version: ^1.24.2
- name: Check input version
if: github.event_name == 'workflow_dispatch'
run: |-
@@ -66,7 +66,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.24
go-version: ^1.24.2
- name: Setup Android NDK
if: matrix.os == 'android'
uses: nttld/setup-ndk@v1
@@ -109,6 +109,11 @@ jobs:
if: contains(needs.calculate_version.outputs.version, '-')
run: |-
echo "NAME=sing-box-beta" >> "$GITHUB_ENV"
- name: Set version
run: |-
PKG_VERSION="${{ needs.calculate_version.outputs.version }}"
PKG_VERSION="${PKG_VERSION//-/\~}"
echo "PKG_VERSION=${PKG_VERSION}" >> "${GITHUB_ENV}"
- name: Package DEB
if: matrix.debian != ''
run: |
@@ -117,7 +122,7 @@ jobs:
sudo apt-get install -y debsigs
fpm -t deb \
--name "${NAME}" \
-v "${{ needs.calculate_version.outputs.version }}" \
-v "$PKG_VERSION" \
-p "dist/${NAME}_${{ needs.calculate_version.outputs.version }}_linux_${{ matrix.debian }}.deb" \
--architecture ${{ matrix.debian }} \
dist/sing-box=/usr/bin/sing-box
@@ -135,7 +140,7 @@ jobs:
sudo gem install fpm
fpm -t rpm \
--name "${NAME}" \
-v "${{ needs.calculate_version.outputs.version }}" \
-v "$PKG_VERSION" \
-p "dist/${NAME}_${{ needs.calculate_version.outputs.version }}_linux_${{ matrix.rpm }}.rpm" \
--architecture ${{ matrix.rpm }} \
dist/sing-box=/usr/bin/sing-box

View File

@@ -50,12 +50,18 @@ nfpms:
contents:
- src: release/config/config.json
dst: /etc/sing-box/config.json
type: config
type: "config|noreplace"
- src: release/config/sing-box.service
dst: /usr/lib/systemd/system/sing-box.service
- src: release/config/sing-box@.service
dst: /usr/lib/systemd/system/sing-box@.service
- src: release/config/sing-box.sysusers
dst: /usr/lib/sysusers.d/sing-box.conf
- src: release/config/sing-box.rules
dst: /usr/share/polkit-1/rules.d/sing-box.rules
- src: release/config/sing-box-split-dns.xml
dst: /usr/share/dbus-1/system.d/sing-box-split-dns.conf
- src: release/completions/sing-box.bash
dst: /usr/share/bash-completion/completions/sing-box.bash

View File

@@ -132,12 +132,18 @@ nfpms:
contents:
- src: release/config/config.json
dst: /etc/sing-box/config.json
type: config
type: "config|noreplace"
- src: release/config/sing-box.service
dst: /usr/lib/systemd/system/sing-box.service
- src: release/config/sing-box@.service
dst: /usr/lib/systemd/system/sing-box@.service
- src: release/config/sing-box.sysusers
dst: /usr/lib/sysusers.d/sing-box.conf
- src: release/config/sing-box.rules
dst: /usr/share/polkit-1/rules.d/sing-box.rules
- src: release/config/sing-box-split-dns.xml
dst: /usr/share/dbus-1/system.d/sing-box-split-dns.conf
- src: release/completions/sing-box.bash
dst: /usr/share/bash-completion/completions/sing-box.bash

View File

@@ -8,7 +8,7 @@ GOHOSTARCH = $(shell go env GOHOSTARCH)
VERSION=$(shell CGO_ENABLED=0 GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH) go run ./cmd/internal/read_tag)
PARAMS = -v -trimpath -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=$(VERSION)' -s -w -buildid="
MAIN_PARAMS = $(PARAMS) -tags $(TAGS)
MAIN_PARAMS = $(PARAMS) -tags "$(TAGS)"
MAIN = ./cmd/sing-box
PREFIX ?= $(shell go env GOPATH)
@@ -24,7 +24,7 @@ ci_build:
go build $(MAIN_PARAMS) $(MAIN)
generate_completions:
go run -v --tags $(TAGS),generate,generate_completions $(MAIN)
go run -v --tags "$(TAGS),generate,generate_completions" $(MAIN)
install:
go build -o $(PREFIX)/bin/$(NAME) $(MAIN_PARAMS) $(MAIN)
@@ -226,8 +226,8 @@ lib:
go run ./cmd/internal/build_libbox -target ios
lib_install:
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.5
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.5
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.6
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.6
docs:
venv/bin/mkdocs serve
@@ -246,4 +246,4 @@ clean:
update:
git fetch
git reset FETCH_HEAD --hard
git clean -fdx
git clean -fdx

View File

@@ -10,9 +10,6 @@ import (
type CertificateStore interface {
LifecycleService
Pool() *x509.CertPool
TLSDecryptionEnabled() bool
TLSDecryptionCertificate() *x509.Certificate
TLSDecryptionPrivateKey() any
}
func RootPoolFromContext(ctx context.Context) *x509.CertPool {

View File

@@ -7,7 +7,9 @@ import (
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"
"github.com/sagernet/sing/common/logger"
"github.com/sagernet/sing/service"
"github.com/miekg/dns"
)
@@ -31,11 +33,30 @@ type DNSClient interface {
}
type DNSQueryOptions struct {
Transport DNSTransport
Strategy C.DomainStrategy
DisableCache bool
RewriteTTL *uint32
ClientSubnet netip.Prefix
Transport DNSTransport
Strategy C.DomainStrategy
LookupStrategy C.DomainStrategy
DisableCache bool
RewriteTTL *uint32
ClientSubnet netip.Prefix
}
func DNSQueryOptionsFrom(ctx context.Context, options *option.DomainResolveOptions) (*DNSQueryOptions, error) {
if options == nil {
return &DNSQueryOptions{}, nil
}
transportManager := service.FromContext[DNSTransportManager](ctx)
transport, loaded := transportManager.Transport(options.Server)
if !loaded {
return nil, E.New("domain resolver not found: " + options.Server)
}
return &DNSQueryOptions{
Transport: transport,
Strategy: C.DomainStrategy(options.Strategy),
DisableCache: options.DisableCache,
RewriteTTL: options.RewriteTTL,
ClientSubnet: options.ClientSubnet.Build(netip.Prefix{}),
}, nil
}
type RDRCStore interface {

View File

@@ -7,7 +7,7 @@ import (
)
type FakeIPStore interface {
Service
SimpleLifecycle
Contains(address netip.Addr) bool
Create(domain string, isIPv6 bool) (netip.Addr, error)
Lookup(address netip.Addr) (string, bool)

View File

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

View File

@@ -37,13 +37,14 @@ func NewManager(logger log.ContextLogger, registry adapter.InboundRegistry, endp
func (m *Manager) Start(stage adapter.StartStage) error {
m.access.Lock()
defer m.access.Unlock()
if m.started && m.stage >= stage {
panic("already started")
}
m.started = true
m.stage = stage
for _, inbound := range m.inbounds {
inbounds := m.inbounds
m.access.Unlock()
for _, inbound := range inbounds {
err := adapter.LegacyStart(inbound, stage)
if err != nil {
return E.Cause(err, stage, " inbound/", inbound.Type(), "[", inbound.Tag(), "]")

View File

@@ -1,8 +1,11 @@
package adapter
import (
E "github.com/sagernet/sing/common/exceptions"
)
import E "github.com/sagernet/sing/common/exceptions"
type SimpleLifecycle interface {
Start() error
Close() error
}
type StartStage uint8
@@ -47,9 +50,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

@@ -28,14 +28,14 @@ func LegacyStart(starter any, stage StartStage) error {
}
type lifecycleServiceWrapper struct {
Service
SimpleLifecycle
name string
}
func NewLifecycleService(service Service, name string) LifecycleService {
func NewLifecycleService(service SimpleLifecycle, name string) LifecycleService {
return &lifecycleServiceWrapper{
Service: service,
name: name,
SimpleLifecycle: service,
name: name,
}
}
@@ -44,9 +44,9 @@ func (l *lifecycleServiceWrapper) Name() string {
}
func (l *lifecycleServiceWrapper) Start(stage StartStage) error {
return LegacyStart(l.Service, stage)
return LegacyStart(l.SimpleLifecycle, stage)
}
func (l *lifecycleServiceWrapper) Close() error {
return l.Service.Close()
return l.SimpleLifecycle.Close()
}

View File

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

View File

@@ -24,7 +24,7 @@ type Router interface {
RuleSet(tag string) (RuleSet, bool)
NeedWIFIState() bool
Rules() []Rule
SetTracker(tracker ConnectionTracker)
AppendTracker(tracker ConnectionTracker)
ResetNetwork()
}

View File

@@ -11,7 +11,7 @@ type HeadlessRule interface {
type Rule interface {
HeadlessRule
Service
SimpleLifecycle
Type() string
Action() RuleAction
}

View File

@@ -1,6 +1,27 @@
package adapter
import (
"context"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
)
type Service interface {
Start() error
Close() error
Lifecycle
Type() string
Tag() string
}
type ServiceRegistry interface {
option.ServiceOptionsRegistry
Create(ctx context.Context, logger log.ContextLogger, tag string, serviceType string, options any) (Service, error)
}
type ServiceManager interface {
Lifecycle
Services() []Service
Get(tag string) (Service, bool)
Remove(tag string) error
Create(ctx context.Context, logger log.ContextLogger, tag string, serviceType string, options any) error
}

View File

@@ -0,0 +1,21 @@
package service
type Adapter struct {
serviceType string
serviceTag string
}
func NewAdapter(serviceType string, serviceTag string) Adapter {
return Adapter{
serviceType: serviceType,
serviceTag: serviceTag,
}
}
func (a *Adapter) Type() string {
return a.serviceType
}
func (a *Adapter) Tag() string {
return a.serviceTag
}

144
adapter/service/manager.go Normal file
View File

@@ -0,0 +1,144 @@
package service
import (
"context"
"os"
"sync"
"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/common"
E "github.com/sagernet/sing/common/exceptions"
)
var _ adapter.ServiceManager = (*Manager)(nil)
type Manager struct {
logger log.ContextLogger
registry adapter.ServiceRegistry
access sync.Mutex
started bool
stage adapter.StartStage
services []adapter.Service
serviceByTag map[string]adapter.Service
}
func NewManager(logger log.ContextLogger, registry adapter.ServiceRegistry) *Manager {
return &Manager{
logger: logger,
registry: registry,
serviceByTag: make(map[string]adapter.Service),
}
}
func (m *Manager) Start(stage adapter.StartStage) error {
m.access.Lock()
if m.started && m.stage >= stage {
panic("already started")
}
m.started = true
m.stage = stage
services := m.services
m.access.Unlock()
for _, service := range services {
err := adapter.LegacyStart(service, stage)
if err != nil {
return E.Cause(err, stage, " service/", service.Type(), "[", service.Tag(), "]")
}
}
return nil
}
func (m *Manager) Close() error {
m.access.Lock()
defer m.access.Unlock()
if !m.started {
return nil
}
m.started = false
services := m.services
m.services = nil
monitor := taskmonitor.New(m.logger, C.StopTimeout)
var err error
for _, service := range services {
monitor.Start("close service/", service.Type(), "[", service.Tag(), "]")
err = E.Append(err, service.Close(), func(err error) error {
return E.Cause(err, "close service/", service.Type(), "[", service.Tag(), "]")
})
monitor.Finish()
}
return nil
}
func (m *Manager) Services() []adapter.Service {
m.access.Lock()
defer m.access.Unlock()
return m.services
}
func (m *Manager) Get(tag string) (adapter.Service, bool) {
m.access.Lock()
service, found := m.serviceByTag[tag]
m.access.Unlock()
return service, found
}
func (m *Manager) Remove(tag string) error {
m.access.Lock()
service, found := m.serviceByTag[tag]
if !found {
m.access.Unlock()
return os.ErrInvalid
}
delete(m.serviceByTag, tag)
index := common.Index(m.services, func(it adapter.Service) bool {
return it == service
})
if index == -1 {
panic("invalid service index")
}
m.services = append(m.services[:index], m.services[index+1:]...)
started := m.started
m.access.Unlock()
if started {
return service.Close()
}
return nil
}
func (m *Manager) Create(ctx context.Context, logger log.ContextLogger, tag string, serviceType string, options any) error {
service, err := m.registry.Create(ctx, logger, tag, serviceType, options)
if err != nil {
return err
}
m.access.Lock()
defer m.access.Unlock()
if m.started {
for _, stage := range adapter.ListStartStages {
err = adapter.LegacyStart(service, stage)
if err != nil {
return E.Cause(err, stage, " service/", service.Type(), "[", service.Tag(), "]")
}
}
}
if existsService, loaded := m.serviceByTag[tag]; loaded {
if m.started {
err = existsService.Close()
if err != nil {
return E.Cause(err, "close service/", existsService.Type(), "[", existsService.Tag(), "]")
}
}
existsIndex := common.Index(m.services, func(it adapter.Service) bool {
return it == existsService
})
if existsIndex == -1 {
panic("invalid service index")
}
m.services = append(m.services[:existsIndex], m.services[existsIndex+1:]...)
}
m.services = append(m.services, service)
m.serviceByTag[tag] = service
return nil
}

View File

@@ -0,0 +1,72 @@
package service
import (
"context"
"sync"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
)
type ConstructorFunc[T any] func(ctx context.Context, logger log.ContextLogger, tag string, options T) (adapter.Service, error)
func Register[Options any](registry *Registry, outboundType string, constructor ConstructorFunc[Options]) {
registry.register(outboundType, func() any {
return new(Options)
}, func(ctx context.Context, logger log.ContextLogger, tag string, rawOptions any) (adapter.Service, error) {
var options *Options
if rawOptions != nil {
options = rawOptions.(*Options)
}
return constructor(ctx, logger, tag, common.PtrValueOrDefault(options))
})
}
var _ adapter.ServiceRegistry = (*Registry)(nil)
type (
optionsConstructorFunc func() any
constructorFunc func(ctx context.Context, logger log.ContextLogger, tag string, options any) (adapter.Service, error)
)
type Registry struct {
access sync.Mutex
optionsType map[string]optionsConstructorFunc
constructor map[string]constructorFunc
}
func NewRegistry() *Registry {
return &Registry{
optionsType: make(map[string]optionsConstructorFunc),
constructor: make(map[string]constructorFunc),
}
}
func (m *Registry) CreateOptions(outboundType string) (any, bool) {
m.access.Lock()
defer m.access.Unlock()
optionsConstructor, loaded := m.optionsType[outboundType]
if !loaded {
return nil, false
}
return optionsConstructor(), true
}
func (m *Registry) Create(ctx context.Context, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Service, error) {
m.access.Lock()
defer m.access.Unlock()
constructor, loaded := m.constructor[outboundType]
if !loaded {
return nil, E.New("outbound type not found: " + outboundType)
}
return constructor(ctx, logger, tag, options)
}
func (m *Registry) register(outboundType string, optionsConstructor optionsConstructorFunc, constructor constructorFunc) {
m.access.Lock()
defer m.access.Unlock()
m.optionsType[outboundType] = optionsConstructor
m.constructor[outboundType] = constructor
}

18
adapter/ssm.go Normal file
View File

@@ -0,0 +1,18 @@
package adapter
import (
"net"
N "github.com/sagernet/sing/common/network"
)
type ManagedSSMServer interface {
Inbound
SetTracker(tracker SSMTracker)
UpdateUsers(users []string, uPSKs []string) error
}
type SSMTracker interface {
TrackConnection(conn net.Conn, metadata InboundContext) net.Conn
TrackPacketConnection(conn N.PacketConn, metadata InboundContext) N.PacketConn
}

View File

@@ -3,6 +3,6 @@ package adapter
import "time"
type TimeService interface {
Service
SimpleLifecycle
TimeFunc() func() time.Time
}

162
box.go
View File

@@ -12,6 +12,7 @@ import (
"github.com/sagernet/sing-box/adapter/endpoint"
"github.com/sagernet/sing-box/adapter/inbound"
"github.com/sagernet/sing-box/adapter/outbound"
boxService "github.com/sagernet/sing-box/adapter/service"
"github.com/sagernet/sing-box/common/certificate"
"github.com/sagernet/sing-box/common/dialer"
"github.com/sagernet/sing-box/common/taskmonitor"
@@ -23,7 +24,6 @@ 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"
@@ -35,23 +35,23 @@ import (
"github.com/sagernet/sing/service/pause"
)
var _ adapter.Service = (*Box)(nil)
var _ adapter.SimpleLifecycle = (*Box)(nil)
type Box struct {
createdAt time.Time
logFactory log.Factory
logger log.ContextLogger
network *route.NetworkManager
endpoint *endpoint.Manager
inbound *inbound.Manager
outbound *outbound.Manager
dnsTransport *dns.TransportManager
dnsRouter *dns.Router
connection *route.ConnectionManager
router *route.Router
mitm adapter.MITMEngine //*mitm.Engine
services []adapter.LifecycleService
done chan struct{}
createdAt time.Time
logFactory log.Factory
logger log.ContextLogger
network *route.NetworkManager
endpoint *endpoint.Manager
inbound *inbound.Manager
outbound *outbound.Manager
service *boxService.Manager
dnsTransport *dns.TransportManager
dnsRouter *dns.Router
connection *route.ConnectionManager
router *route.Router
internalService []adapter.LifecycleService
done chan struct{}
}
type Options struct {
@@ -66,6 +66,7 @@ func Context(
outboundRegistry adapter.OutboundRegistry,
endpointRegistry adapter.EndpointRegistry,
dnsTransportRegistry adapter.DNSTransportRegistry,
serviceRegistry adapter.ServiceRegistry,
) context.Context {
if service.FromContext[option.InboundOptionsRegistry](ctx) == nil ||
service.FromContext[adapter.InboundRegistry](ctx) == nil {
@@ -86,6 +87,10 @@ func Context(
ctx = service.ContextWith[option.DNSTransportOptionsRegistry](ctx, dnsTransportRegistry)
ctx = service.ContextWith[adapter.DNSTransportRegistry](ctx, dnsTransportRegistry)
}
if service.FromContext[adapter.ServiceRegistry](ctx) == nil {
ctx = service.ContextWith[option.ServiceOptionsRegistry](ctx, serviceRegistry)
ctx = service.ContextWith[adapter.ServiceRegistry](ctx, serviceRegistry)
}
return ctx
}
@@ -101,6 +106,7 @@ func New(options Options) (*Box, error) {
inboundRegistry := service.FromContext[adapter.InboundRegistry](ctx)
outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx)
dnsTransportRegistry := service.FromContext[adapter.DNSTransportRegistry](ctx)
serviceRegistry := service.FromContext[adapter.ServiceRegistry](ctx)
if endpointRegistry == nil {
return nil, E.New("missing endpoint registry in context")
@@ -111,6 +117,12 @@ func New(options Options) (*Box, error) {
if outboundRegistry == nil {
return nil, E.New("missing outbound registry in context")
}
if dnsTransportRegistry == nil {
return nil, E.New("missing DNS transport registry in context")
}
if serviceRegistry == nil {
return nil, E.New("missing service registry in context")
}
ctx = pause.WithDefaultManager(ctx)
experimentalOptions := common.PtrValueOrDefault(options.Experimental)
@@ -144,13 +156,19 @@ func New(options Options) (*Box, error) {
return nil, E.Cause(err, "create log factory")
}
var services []adapter.LifecycleService
certificateStore, err := certificate.NewStore(ctx, logFactory.NewLogger("certificate"), common.PtrValueOrDefault(options.Certificate))
if err != nil {
return nil, err
var internalServices []adapter.LifecycleService
certificateOptions := common.PtrValueOrDefault(options.Certificate)
if C.IsAndroid || certificateOptions.Store != "" && certificateOptions.Store != C.CertificateStoreSystem ||
len(certificateOptions.Certificate) > 0 ||
len(certificateOptions.CertificatePath) > 0 ||
len(certificateOptions.CertificateDirectoryPath) > 0 {
certificateStore, err := certificate.NewStore(ctx, logFactory.NewLogger("certificate"), certificateOptions)
if err != nil {
return nil, err
}
service.MustRegister[adapter.CertificateStore](ctx, certificateStore)
internalServices = append(internalServices, certificateStore)
}
service.MustRegister[adapter.CertificateStore](ctx, certificateStore)
services = append(services, certificateStore)
routeOptions := common.PtrValueOrDefault(options.Route)
dnsOptions := common.PtrValueOrDefault(options.DNS)
@@ -158,10 +176,12 @@ func New(options Options) (*Box, error) {
inboundManager := inbound.NewManager(logFactory.NewLogger("inbound"), inboundRegistry, endpointManager)
outboundManager := outbound.NewManager(logFactory.NewLogger("outbound"), outboundRegistry, endpointManager, routeOptions.Final)
dnsTransportManager := dns.NewTransportManager(logFactory.NewLogger("dns/transport"), dnsTransportRegistry, outboundManager, dnsOptions.Final)
serviceManager := boxService.NewManager(logFactory.NewLogger("service"), serviceRegistry)
service.MustRegister[adapter.EndpointManager](ctx, endpointManager)
service.MustRegister[adapter.InboundManager](ctx, inboundManager)
service.MustRegister[adapter.OutboundManager](ctx, outboundManager)
service.MustRegister[adapter.DNSTransportManager](ctx, dnsTransportManager)
service.MustRegister[adapter.ServiceManager](ctx, serviceManager)
dnsRouter := dns.NewRouter(ctx, logFactory, dnsOptions)
service.MustRegister[adapter.DNSRouter](ctx, dnsRouter)
networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions)
@@ -169,7 +189,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)
@@ -177,8 +197,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)
@@ -276,6 +296,24 @@ func New(options Options) (*Box, error) {
return nil, E.Cause(err, "initialize outbound[", i, "]")
}
}
for i, serviceOptions := range options.Services {
var tag string
if serviceOptions.Tag != "" {
tag = serviceOptions.Tag
} else {
tag = F.ToString(i)
}
err = serviceManager.Create(
ctx,
logFactory.NewLogger(F.ToString("service/", serviceOptions.Type, "[", tag, "]")),
tag,
serviceOptions.Type,
serviceOptions.Options,
)
if err != nil {
return nil, E.Cause(err, "initialize service[", i, "]")
}
}
outboundManager.Initialize(common.Must1(
direct.NewOutbound(
ctx,
@@ -301,7 +339,7 @@ func New(options Options) (*Box, error) {
if needCacheFile {
cacheFile := cachefile.New(ctx, common.PtrValueOrDefault(experimentalOptions.CacheFile))
service.MustRegister[adapter.CacheFile](ctx, cacheFile)
services = append(services, cacheFile)
internalServices = append(internalServices, cacheFile)
}
if needClashAPI {
clashAPIOptions := common.PtrValueOrDefault(experimentalOptions.ClashAPI)
@@ -310,9 +348,9 @@ func New(options Options) (*Box, error) {
if err != nil {
return nil, E.Cause(err, "create clash-server")
}
router.SetTracker(clashServer)
router.AppendTracker(clashServer)
service.MustRegister[adapter.ClashServer](ctx, clashServer)
services = append(services, clashServer)
internalServices = append(internalServices, clashServer)
}
if needV2RayAPI {
v2rayServer, err := experimental.NewV2RayServer(logFactory.NewLogger("v2ray-api"), common.PtrValueOrDefault(experimentalOptions.V2RayAPI))
@@ -320,8 +358,8 @@ func New(options Options) (*Box, error) {
return nil, E.Cause(err, "create v2ray-server")
}
if v2rayServer.StatsService() != nil {
router.SetTracker(v2rayServer.StatsService())
services = append(services, v2rayServer)
router.AppendTracker(v2rayServer.StatsService())
internalServices = append(internalServices, v2rayServer)
service.MustRegister[adapter.V2RayServer](ctx, v2rayServer)
}
}
@@ -339,33 +377,23 @@ func New(options Options) (*Box, error) {
WriteToSystem: ntpOptions.WriteToSystem,
})
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
internalServices = append(internalServices, adapter.NewLifecycleService(ntpService, "ntp service"))
}
return &Box{
network: networkManager,
endpoint: endpointManager,
inbound: inboundManager,
outbound: outboundManager,
dnsTransport: dnsTransportManager,
dnsRouter: dnsRouter,
connection: connectionManager,
router: router,
mitm: mitmEngine,
createdAt: createdAt,
logFactory: logFactory,
logger: logFactory.Logger(),
services: services,
done: make(chan struct{}),
network: networkManager,
endpoint: endpointManager,
inbound: inboundManager,
outbound: outboundManager,
dnsTransport: dnsTransportManager,
service: serviceManager,
dnsRouter: dnsRouter,
connection: connectionManager,
router: router,
createdAt: createdAt,
logFactory: logFactory,
logger: logFactory.Logger(),
internalService: internalServices,
done: make(chan struct{}),
}, nil
}
@@ -415,15 +443,15 @@ func (s *Box) preStart() error {
if err != nil {
return E.Cause(err, "start logger")
}
err = adapter.StartNamed(adapter.StartStateInitialize, s.services) // cache-file clash-api v2ray-api
err = adapter.StartNamed(adapter.StartStateInitialize, s.internalService) // cache-file clash-api v2ray-api
if err != nil {
return err
}
err = adapter.Start(adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, 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, s.service)
if err != nil {
return err
}
err = adapter.Start(adapter.StartStateStart, s.outbound, s.dnsTransport, s.dnsRouter, s.network, s.connection, s.router, s.mitm)
err = adapter.Start(adapter.StartStateStart, s.outbound, s.dnsTransport, s.dnsRouter, s.network, s.connection, s.router)
if err != nil {
return err
}
@@ -435,31 +463,27 @@ func (s *Box) start() error {
if err != nil {
return err
}
err = adapter.StartNamed(adapter.StartStateStart, s.services)
err = adapter.StartNamed(adapter.StartStateStart, s.internalService)
if err != nil {
return err
}
err = s.inbound.Start(adapter.StartStateStart)
err = adapter.Start(adapter.StartStateStart, s.inbound, s.endpoint, s.service)
if err != nil {
return err
}
err = adapter.Start(adapter.StartStateStart, s.endpoint)
err = adapter.Start(adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.inbound, s.endpoint, s.service)
if err != nil {
return err
}
err = adapter.Start(adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.mitm, s.inbound, s.endpoint)
err = adapter.StartNamed(adapter.StartStatePostStart, s.internalService)
if err != nil {
return err
}
err = adapter.StartNamed(adapter.StartStatePostStart, s.services)
err = adapter.Start(adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service)
if err != nil {
return err
}
err = adapter.Start(adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.mitm, s.outbound, s.inbound, s.endpoint)
if err != nil {
return err
}
err = adapter.StartNamed(adapter.StartStateStarted, s.services)
err = adapter.StartNamed(adapter.StartStateStarted, s.internalService)
if err != nil {
return err
}
@@ -474,9 +498,9 @@ func (s *Box) Close() error {
close(s.done)
}
err := common.Close(
s.inbound, s.outbound, s.endpoint, s.mitm, 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 {
for _, lifecycleService := range s.internalService {
err = E.Append(err, lifecycleService.Close(), func(err error) error {
return E.Cause(err, "close ", lifecycleService.Name())
})

View File

@@ -27,11 +27,8 @@ func main() {
)
if flagRunNightly {
var version badversion.Version
version, err = build_shared.ReadTagVersionRev()
version, err = build_shared.ReadTagVersion()
if err == nil {
if version.PreReleaseIdentifier == "" {
version.Patch++
}
versionStr = version.String()
}
} else {

View File

@@ -69,5 +69,5 @@ func preRun(cmd *cobra.Command, args []string) {
configPaths = append(configPaths, "config.json")
}
globalCtx = service.ContextWith(globalCtx, deprecated.NewStderrManager(log.StdLogger()))
globalCtx = box.Context(globalCtx, include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry(), include.DNSTransportRegistry())
globalCtx = box.Context(globalCtx, include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry(), include.DNSTransportRegistry(), include.ServiceRegistry())
}

View File

@@ -1,120 +0,0 @@
package main
import (
"crypto/rand"
"crypto/rsa"
"crypto/sha1"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/base64"
"encoding/hex"
"encoding/pem"
"math/big"
"os"
"path/filepath"
"strings"
"time"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/json"
"github.com/spf13/cobra"
"software.sslmate.com/src/go-pkcs12"
)
var (
flagGenerateCAName string
flagGenerateCAPKCS12Password string
flagGenerateOutput string
)
var commandGenerateCAKeyPair = &cobra.Command{
Use: "ca-keypair",
Short: "Generate CA key pair",
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
err := generateCAKeyPair()
if err != nil {
log.Fatal(err)
}
},
}
func init() {
commandGenerateCAKeyPair.Flags().StringVarP(&flagGenerateCAName, "name", "n", "", "Set custom CA name")
commandGenerateCAKeyPair.Flags().StringVarP(&flagGenerateCAPKCS12Password, "p12-password", "p", "", "Set custom PKCS12 password")
commandGenerateCAKeyPair.Flags().StringVarP(&flagGenerateOutput, "output", "o", ".", "Set output directory")
commandGenerate.AddCommand(commandGenerateCAKeyPair)
}
func generateCAKeyPair() error {
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return err
}
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
return err
}
spkiASN1, err := x509.MarshalPKIXPublicKey(privateKey.Public())
var spki struct {
Algorithm pkix.AlgorithmIdentifier
SubjectPublicKey asn1.BitString
}
_, err = asn1.Unmarshal(spkiASN1, &spki)
if err != nil {
return err
}
skid := sha1.Sum(spki.SubjectPublicKey.Bytes)
var caName string
if flagGenerateCAName != "" {
caName = flagGenerateCAName
} else {
caName = "sing-box Generated CA " + strings.ToUpper(hex.EncodeToString(skid[:4]))
}
caTpl := &x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
Organization: []string{caName},
CommonName: caName,
},
SubjectKeyId: skid[:],
NotAfter: time.Now().AddDate(10, 0, 0),
NotBefore: time.Now(),
KeyUsage: x509.KeyUsageCertSign,
BasicConstraintsValid: true,
IsCA: true,
MaxPathLenZero: true,
}
publicDer, err := x509.CreateCertificate(rand.Reader, caTpl, caTpl, privateKey.Public(), privateKey)
var caPassword string
if flagGenerateCAPKCS12Password != "" {
caPassword = flagGenerateCAPKCS12Password
} else {
caPassword = strings.ToUpper(hex.EncodeToString(skid[:4]))
}
caTpl.Raw = publicDer
p12Bytes, err := pkcs12.Modern.Encode(privateKey, caTpl, nil, caPassword)
if err != nil {
return err
}
privateDer, err := x509.MarshalPKCS8PrivateKey(privateKey)
if err != nil {
return err
}
os.WriteFile(filepath.Join(flagGenerateOutput, caName+".pem"), []byte(string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: publicDer}))+string(pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privateDer}))), 0o644)
os.WriteFile(filepath.Join(flagGenerateOutput, caName+".crt"), publicDer, 0o644)
os.WriteFile(filepath.Join(flagGenerateOutput, caName+".p12"), p12Bytes, 0o644)
var tlsDecryptionOptions option.TLSDecryptionOptions
tlsDecryptionOptions.Enabled = true
tlsDecryptionOptions.KeyPair = base64.StdEncoding.EncodeToString(p12Bytes)
tlsDecryptionOptions.KeyPairPassword = caPassword
var certificateOptions option.CertificateOptions
certificateOptions.TLSDecryption = &tlsDecryptionOptions
encoder := json.NewEncoder(os.Stdout)
encoder.SetIndent("", " ")
return encoder.Encode(certificateOptions)
}

View File

@@ -1,6 +1,13 @@
package main
import (
"errors"
"os"
"github.com/sagernet/sing-box"
E "github.com/sagernet/sing/common/exceptions"
N "github.com/sagernet/sing/common/network"
"github.com/spf13/cobra"
)
@@ -12,5 +19,36 @@ var commandTools = &cobra.Command{
}
func init() {
commandTools.PersistentFlags().StringVarP(&commandToolsFlagOutbound, "outbound", "o", "", "Use specified tag instead of default outbound")
mainCommand.AddCommand(commandTools)
}
func createPreStartedClient() (*box.Box, error) {
options, err := readConfigAndMerge()
if err != nil {
if !(errors.Is(err, os.ErrNotExist) && len(configDirectories) == 0 && len(configPaths) == 1) || configPaths[0] != "config.json" {
return nil, err
}
}
instance, err := box.New(box.Options{Context: globalCtx, Options: options})
if err != nil {
return nil, E.Cause(err, "create service")
}
err = instance.PreStart()
if err != nil {
return nil, E.Cause(err, "start service")
}
return instance, nil
}
func createDialer(instance *box.Box, outboundTag string) (N.Dialer, error) {
if outboundTag == "" {
return instance.Outbound().Default(), nil
} else {
outbound, loaded := instance.Outbound().Outbound(outboundTag)
if !loaded {
return nil, E.New("outbound not found: ", outboundTag)
}
return outbound, nil
}
}

View File

@@ -0,0 +1,73 @@
package main
import (
"context"
"os"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/bufio"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/task"
"github.com/spf13/cobra"
)
var commandConnectFlagNetwork string
var commandConnect = &cobra.Command{
Use: "connect <address>",
Short: "Connect to an address",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
err := connect(args[0])
if err != nil {
log.Fatal(err)
}
},
}
func init() {
commandConnect.Flags().StringVarP(&commandConnectFlagNetwork, "network", "n", "tcp", "network type")
commandTools.AddCommand(commandConnect)
}
func connect(address string) error {
switch N.NetworkName(commandConnectFlagNetwork) {
case N.NetworkTCP, N.NetworkUDP:
default:
return E.Cause(N.ErrUnknownNetwork, commandConnectFlagNetwork)
}
instance, err := createPreStartedClient()
if err != nil {
return err
}
defer instance.Close()
dialer, err := createDialer(instance, commandToolsFlagOutbound)
if err != nil {
return err
}
conn, err := dialer.DialContext(context.Background(), commandConnectFlagNetwork, M.ParseSocksaddr(address))
if err != nil {
return E.Cause(err, "connect to server")
}
var group task.Group
group.Append("upload", func(ctx context.Context) error {
return common.Error(bufio.Copy(conn, os.Stdin))
})
group.Append("download", func(ctx context.Context) error {
return common.Error(bufio.Copy(os.Stdout, conn))
})
group.Cleanup(func() {
conn.Close()
})
err = group.Run(context.Background())
if E.IsClosed(err) {
log.Info(err)
} else {
log.Error(err)
}
return nil
}

View File

@@ -0,0 +1,115 @@
package main
import (
"context"
"errors"
"io"
"net"
"net/http"
"net/url"
"os"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common/bufio"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
"github.com/spf13/cobra"
)
var commandFetch = &cobra.Command{
Use: "fetch",
Short: "Fetch an URL",
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
err := fetch(args)
if err != nil {
log.Fatal(err)
}
},
}
func init() {
commandTools.AddCommand(commandFetch)
}
var (
httpClient *http.Client
http3Client *http.Client
)
func fetch(args []string) error {
instance, err := createPreStartedClient()
if err != nil {
return err
}
defer instance.Close()
httpClient = &http.Client{
Transport: &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
dialer, err := createDialer(instance, commandToolsFlagOutbound)
if err != nil {
return nil, err
}
return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
},
ForceAttemptHTTP2: true,
},
}
defer httpClient.CloseIdleConnections()
if C.WithQUIC {
err = initializeHTTP3Client(instance)
if err != nil {
return err
}
defer http3Client.CloseIdleConnections()
}
for _, urlString := range args {
var parsedURL *url.URL
parsedURL, err = url.Parse(urlString)
if err != nil {
return err
}
switch parsedURL.Scheme {
case "":
parsedURL.Scheme = "http"
fallthrough
case "http", "https":
err = fetchHTTP(httpClient, parsedURL)
if err != nil {
return err
}
case "http3":
if !C.WithQUIC {
return C.ErrQUICNotIncluded
}
parsedURL.Scheme = "https"
err = fetchHTTP(http3Client, parsedURL)
if err != nil {
return err
}
default:
return E.New("unsupported scheme: ", parsedURL.Scheme)
}
}
return nil
}
func fetchHTTP(httpClient *http.Client, parsedURL *url.URL) error {
request, err := http.NewRequest("GET", parsedURL.String(), nil)
if err != nil {
return err
}
request.Header.Add("User-Agent", "curl/7.88.0")
response, err := httpClient.Do(request)
if err != nil {
return err
}
defer response.Body.Close()
_, err = bufio.Copy(os.Stdout, response.Body)
if errors.Is(err, io.EOF) {
return nil
}
return err
}

View File

@@ -0,0 +1,36 @@
//go:build with_quic
package main
import (
"context"
"crypto/tls"
"net/http"
"github.com/sagernet/quic-go"
"github.com/sagernet/quic-go/http3"
box "github.com/sagernet/sing-box"
"github.com/sagernet/sing/common/bufio"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
func initializeHTTP3Client(instance *box.Box) error {
dialer, err := createDialer(instance, commandToolsFlagOutbound)
if err != nil {
return err
}
http3Client = &http.Client{
Transport: &http3.Transport{
Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
destination := M.ParseSocksaddr(addr)
udpConn, dErr := dialer.DialContext(ctx, N.NetworkUDP, destination)
if dErr != nil {
return nil, dErr
}
return quic.DialEarly(ctx, bufio.NewUnbindPacketConn(udpConn), udpConn.RemoteAddr(), tlsCfg, cfg)
},
},
}
return nil
}

View File

@@ -0,0 +1,18 @@
//go:build !with_quic
package main
import (
"net/url"
"os"
box "github.com/sagernet/sing-box"
)
func initializeHTTP3Client(instance *box.Box) error {
return os.ErrInvalid
}
func fetchHTTP3(parsedURL *url.URL) error {
return os.ErrInvalid
}

View File

@@ -1,108 +0,0 @@
package main
import (
"encoding/pem"
"errors"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"github.com/sagernet/sing-box/log"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/shell"
"github.com/spf13/cobra"
)
var commandInstallCACertificate = &cobra.Command{
Use: "install-ca <path to certificate>",
Short: "Install CA certificate to system",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
err := installCACertificate(args[0])
if err != nil {
log.Fatal(err)
}
},
}
func init() {
commandTools.AddCommand(commandInstallCACertificate)
}
func installCACertificate(path string) error {
switch runtime.GOOS {
case "windows":
return shell.Exec("powershell", "-Command", "Import-Certificate -FilePath \""+path+"\" -CertStoreLocation Cert:\\LocalMachine\\Root").Attach().Run()
case "darwin":
return shell.Exec("sudo", "security", "add-trusted-cert", "-d", "-r", "trustRoot", "-k", "/Library/Keychains/System.keychain", path).Attach().Run()
case "linux":
updateCertPath, updateCertPathNotFoundErr := exec.LookPath("update-ca-certificates")
if updateCertPathNotFoundErr == nil {
publicDer, err := os.ReadFile(path)
if err != nil {
return err
}
err = os.MkdirAll("/usr/local/share/ca-certificates", 0o755)
if err != nil {
if errors.Is(err, os.ErrPermission) {
log.Info("Try running with sudo")
return shell.Exec("sudo", os.Args...).Attach().Run()
}
return err
}
fileName := filepath.Base(updateCertPath)
if !strings.HasSuffix(fileName, ".crt") {
fileName = fileName + ".crt"
}
filePath, _ := filepath.Abs(filepath.Join("/usr/local/share/ca-certificates", fileName))
err = os.WriteFile(filePath, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: publicDer}), 0o644)
if err != nil {
if errors.Is(err, os.ErrPermission) {
log.Info("Try running with sudo")
return shell.Exec("sudo", os.Args...).Attach().Run()
}
return err
}
log.Info("certificate written to " + filePath + "\n")
err = shell.Exec(updateCertPath).Attach().Run()
if err != nil {
return err
}
log.Info("certificate installed")
return nil
}
updateTrustPath, updateTrustPathNotFoundErr := exec.LookPath("update-ca-trust")
if updateTrustPathNotFoundErr == nil {
publicDer, err := os.ReadFile(path)
if err != nil {
return err
}
fileName := filepath.Base(updateTrustPath)
fileExt := filepath.Ext(path)
if fileExt != "" {
fileName = fileName[:len(fileName)-len(fileExt)]
}
filePath, _ := filepath.Abs(filepath.Join("/etc/pki/ca-trust/source/anchors/", fileName+".pem"))
err = os.WriteFile(filePath, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: publicDer}), 0o644)
if err != nil {
if errors.Is(err, os.ErrPermission) {
log.Info("Try running with sudo")
return shell.Exec("sudo", os.Args...).Attach().Run()
}
return err
}
log.Info("certificate written to " + filePath + "\n")
err = shell.Exec(updateTrustPath, "extract").Attach().Run()
if err != nil {
return err
}
log.Info("certificate installed")
}
return E.New("update-ca-certificates or update-ca-trust not found")
default:
return E.New("unsupported operating system: ", runtime.GOOS)
}
}

View File

@@ -8,7 +8,6 @@ import (
"github.com/sagernet/sing-box/log"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/ntp"
"github.com/spf13/cobra"
@@ -40,11 +39,20 @@ func init() {
}
func syncTime() error {
instance, err := createPreStartedClient()
if err != nil {
return err
}
dialer, err := createDialer(instance, commandToolsFlagOutbound)
if err != nil {
return err
}
defer instance.Close()
serverAddress := M.ParseSocksaddr(commandSyncTimeFlagServer)
if serverAddress.Port == 0 {
serverAddress.Port = 123
}
response, err := ntp.Exchange(context.Background(), N.SystemDialer, serverAddress)
response, err := ntp.Exchange(context.Background(), dialer, serverAddress)
if err != nil {
return err
}

View File

@@ -3,7 +3,6 @@ package certificate
import (
"context"
"crypto/x509"
"encoding/base64"
"io/fs"
"os"
"path/filepath"
@@ -17,8 +16,6 @@ import (
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
"github.com/sagernet/sing/service"
"software.sslmate.com/src/go-pkcs12"
)
var _ adapter.CertificateStore = (*Store)(nil)
@@ -30,9 +27,6 @@ type Store struct {
certificatePaths []string
certificateDirectoryPaths []string
watcher *fswatch.Watcher
tlsDecryptionEnabled bool
tlsDecryptionPrivateKey any
tlsDecryptionCertificate *x509.Certificate
}
func NewStore(ctx context.Context, logger logger.Logger, options option.CertificateOptions) (*Store, error) {
@@ -96,19 +90,6 @@ func NewStore(ctx context.Context, logger logger.Logger, options option.Certific
if err != nil {
return nil, E.Cause(err, "initializing certificate store")
}
if options.TLSDecryption != nil && options.TLSDecryption.Enabled {
pfxBytes, err := base64.StdEncoding.DecodeString(options.TLSDecryption.KeyPair)
if err != nil {
return nil, E.Cause(err, "decode key pair base64 bytes")
}
privateKey, certificate, err := pkcs12.Decode(pfxBytes, options.TLSDecryption.KeyPairPassword)
if err != nil {
return nil, E.Cause(err, "decode key pair")
}
store.tlsDecryptionEnabled = true
store.tlsDecryptionPrivateKey = privateKey
store.tlsDecryptionCertificate = certificate
}
return store, nil
}
@@ -202,15 +183,3 @@ func isSameDirSymlink(f fs.DirEntry, dir string) bool {
target, err := os.Readlink(filepath.Join(dir, f.Name()))
return err == nil && !strings.Contains(target, "/")
}
func (s *Store) TLSDecryptionEnabled() bool {
return s.tlsDecryptionEnabled
}
func (s *Store) TLSDecryptionCertificate() *x509.Certificate {
return s.tlsDecryptionCertificate
}
func (s *Store) TLSDecryptionPrivateKey() any {
return s.tlsDecryptionPrivateKey
}

View File

@@ -71,18 +71,8 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
listener.Control = control.Append(listener.Control, bindFunc)
}
if options.RoutingMark > 0 {
dialer.Control = control.Append(dialer.Control, control.RoutingMark(uint32(options.RoutingMark)))
listener.Control = control.Append(listener.Control, control.RoutingMark(uint32(options.RoutingMark)))
}
if networkManager != nil {
autoRedirectOutputMark := networkManager.AutoRedirectOutputMark()
if autoRedirectOutputMark > 0 {
if options.RoutingMark > 0 {
return nil, E.New("`routing_mark` is conflict with `tun.auto_redirect` with `tun.route_[_exclude]_address_set")
}
dialer.Control = control.Append(dialer.Control, control.RoutingMark(autoRedirectOutputMark))
listener.Control = control.Append(listener.Control, control.RoutingMark(autoRedirectOutputMark))
}
dialer.Control = control.Append(dialer.Control, setMarkWrapper(networkManager, uint32(options.RoutingMark), false))
listener.Control = control.Append(listener.Control, setMarkWrapper(networkManager, uint32(options.RoutingMark), false))
}
disableDefaultBind := options.BindInterface != "" || options.Inet4BindAddress != nil || options.Inet6BindAddress != nil
if disableDefaultBind || options.TCPFastOpen {
@@ -127,8 +117,8 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
}
}
if options.RoutingMark == 0 && defaultOptions.RoutingMark != 0 {
dialer.Control = control.Append(dialer.Control, control.RoutingMark(defaultOptions.RoutingMark))
listener.Control = control.Append(listener.Control, control.RoutingMark(defaultOptions.RoutingMark))
dialer.Control = control.Append(dialer.Control, setMarkWrapper(networkManager, defaultOptions.RoutingMark, true))
listener.Control = control.Append(listener.Control, setMarkWrapper(networkManager, defaultOptions.RoutingMark, true))
}
}
if options.ReuseAddr {
@@ -210,6 +200,22 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
}, nil
}
func setMarkWrapper(networkManager adapter.NetworkManager, mark uint32, isDefault bool) control.Func {
if networkManager == nil {
return control.RoutingMark(mark)
}
return func(network, address string, conn syscall.RawConn) error {
if networkManager.AutoRedirectOutputMark() != 0 {
if isDefault {
return E.New("`route.default_mark` is conflict with `tun.auto_redirect`")
} else {
return E.New("`routing_mark` is conflict with `tun.auto_redirect`")
}
}
return control.RoutingMark(mark)(network, address, conn)
}
}
func (d *DefaultDialer) DialContext(ctx context.Context, network string, address M.Socksaddr) (net.Conn, error) {
if !address.IsValid() {
return nil, E.New("invalid address")

View File

@@ -24,6 +24,7 @@ type Options struct {
ResolverOnDetour bool
NewDialer bool
LegacyDNSDialer bool
DirectOutbound bool
}
// TODO: merge with NewWithOptions
@@ -102,13 +103,13 @@ 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 {
transports := dnsTransport.Transports()
if len(transports) < 2 {
dnsQueryOptions.Transport = dnsTransport.Default()
} else {
} else if options.NewDialer {
return nil, E.New("missing domain resolver for domain server address")
} else if !options.DirectOutbound {
deprecated.Report(options.Context, deprecated.OptionMissingDomainResolver)
}
}

View File

@@ -9,6 +9,7 @@ import (
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
E "github.com/sagernet/sing/common/exceptions"
)
const (
@@ -23,7 +24,7 @@ func BitTorrent(_ context.Context, metadata *adapter.InboundContext, reader io.R
var first byte
err := binary.Read(reader, binary.BigEndian, &first)
if err != nil {
return err
return E.Cause1(ErrNeedMoreData, err)
}
if first != 19 {
@@ -33,7 +34,7 @@ func BitTorrent(_ context.Context, metadata *adapter.InboundContext, reader io.R
var protocol [19]byte
_, err = reader.Read(protocol[:])
if err != nil {
return err
return E.Cause1(ErrNeedMoreData, err)
}
if string(protocol[:]) != "BitTorrent protocol" {
return os.ErrInvalid
@@ -67,7 +68,9 @@ func UTP(_ context.Context, metadata *adapter.InboundContext, packet []byte) err
if err != nil {
return err
}
if extension > 0x04 {
return os.ErrInvalid
}
var length byte
err = binary.Read(reader, binary.BigEndian, &length)
if err != nil {

View File

@@ -71,3 +71,19 @@ func TestSniffUDPTracker(t *testing.T) {
require.Equal(t, C.ProtocolBitTorrent, metadata.Protocol)
}
}
func TestSniffNotUTP(t *testing.T) {
t.Parallel()
packets := []string{
"0102736470696e674958d580121500000000000079aaed6717a39c27b07c0c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
}
for _, pkt := range packets {
pkt, err := hex.DecodeString(pkt)
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.UTP(context.TODO(), &metadata, pkt)
require.Error(t, err)
}
}

View File

@@ -5,14 +5,11 @@ import (
"encoding/binary"
"io"
"os"
"time"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/buf"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/common/task"
E "github.com/sagernet/sing/common/exceptions"
mDNS "github.com/miekg/dns"
)
@@ -21,22 +18,16 @@ func StreamDomainNameQuery(readCtx context.Context, metadata *adapter.InboundCon
var length uint16
err := binary.Read(reader, binary.BigEndian, &length)
if err != nil {
return os.ErrInvalid
return E.Cause1(ErrNeedMoreData, err)
}
if length == 0 {
return os.ErrInvalid
}
buffer := buf.NewSize(int(length))
defer buffer.Release()
readCtx, cancel := context.WithTimeout(readCtx, time.Millisecond*100)
var readTask task.Group
readTask.Append0(func(ctx context.Context) error {
return common.Error(buffer.ReadFullFrom(reader, buffer.FreeLen()))
})
err = readTask.Run(readCtx)
cancel()
_, err = buffer.ReadFullFrom(reader, buffer.FreeLen())
if err != nil {
return err
return E.Cause1(ErrNeedMoreData, err)
}
return DomainNameQuery(readCtx, metadata, buffer.Bytes())
}
@@ -47,9 +38,6 @@ func DomainNameQuery(ctx context.Context, metadata *adapter.InboundContext, pack
if err != nil {
return err
}
if len(msg.Question) == 0 || msg.Question[0].Qclass != mDNS.ClassINET || !M.IsDomainName(msg.Question[0].Name) {
return os.ErrInvalid
}
metadata.Protocol = C.ProtocolDNS
return nil
}

23
common/sniff/dns_test.go Normal file
View File

@@ -0,0 +1,23 @@
package sniff_test
import (
"context"
"encoding/hex"
"testing"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/sniff"
C "github.com/sagernet/sing-box/constant"
"github.com/stretchr/testify/require"
)
func TestSniffDNS(t *testing.T) {
t.Parallel()
query, err := hex.DecodeString("740701000001000000000000012a06676f6f676c6503636f6d0000010001")
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.DomainNameQuery(context.TODO(), &metadata, query)
require.NoError(t, err)
require.Equal(t, C.ProtocolDNS, metadata.Protocol)
}

View File

@@ -3,10 +3,12 @@ package sniff
import (
std_bufio "bufio"
"context"
"errors"
"io"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/protocol/http"
)
@@ -14,10 +16,13 @@ import (
func HTTPHost(_ context.Context, metadata *adapter.InboundContext, reader io.Reader) error {
request, err := http.ReadRequest(std_bufio.NewReader(reader))
if err != nil {
return err
if errors.Is(err, io.ErrUnexpectedEOF) {
return E.Cause1(ErrNeedMoreData, err)
} else {
return err
}
}
metadata.Protocol = C.ProtocolHTTP
metadata.Domain = M.ParseSocksaddr(request.Host).AddrString()
metadata.HTTPRequest = request
return nil
}

View File

@@ -20,8 +20,6 @@ import (
"golang.org/x/crypto/hkdf"
)
var ErrClientHelloFragmented = E.New("need more packet for chromium QUIC connection")
func QUICClientHello(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error {
reader := bytes.NewReader(packet)
typeByte, err := reader.ReadByte()
@@ -308,7 +306,7 @@ find:
metadata.Protocol = C.ProtocolQUIC
metadata.Client = C.ClientChromium
metadata.SniffContext = fragments
return ErrClientHelloFragmented
return E.Cause1(ErrNeedMoreData, err)
}
metadata.Domain = fingerprint.ServerName
for metadata.Client == "" {

View File

@@ -20,11 +20,11 @@ func TestSniffQUICChromeNew(t *testing.T) {
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
require.Equal(t, metadata.Protocol, C.ProtocolQUIC)
require.Equal(t, metadata.Client, C.ClientChromium)
require.ErrorIs(t, err, sniff.ErrClientHelloFragmented)
require.ErrorIs(t, err, sniff.ErrNeedMoreData)
pkt, err = hex.DecodeString("cc0000000108e241a0c601413b4f004046006d8f15dae9999edf39d58df6762822b9a2ab996d7f6a10044338af3b51b1814bc4ac0fa5a87c34c6ae604af8cabc5957c5240174deefc8e378719ffdab2ae4e15bf4514bea44894b626c685cd5d5c965f7e97b3a1bdc520b75813e747f37a3ae83ad38b9ca2acb0de4fc9424839a50c8fb815a62b498609fbbc59145698860e0509cc08a04d1b119daef844ba2f09c16e2665e5cc0b47624b71f7b950c54fd56b4a1fbb826cba44eeeee3949ced8f5de60d4c81b19ee59f75aa1abb33f22c6b13c27095eb1e99cff01fdc93e6e88da2622ee18c08a79f508befd7e33e99bca60e64bef9a47b764384bd93823daeeb6fcb4d7cfbc4ab53eff59b3636f6dcaaf229b5a94941b5712807166b9bd5e82cb4a9708a71451c4cd6f6e33fb2fe40c8c70dd51a30b37ff9c5e35783debde0093fde19ce074b4887b3c90980b107b9c0f32cf61a66f37c251b789abc4d27fc421207966846c8cc7faa42d9af6ad355a6bc94cb78223b612be8b3e2a4df61fee83a674a0ceb8b7c3a29b97102cda22fecdf6a4628e5b612bc17eab64d6f75feedd0b106c0419e484e66725759964cb5935ac5125e5ae920cd280bd40df57c1d7ae1845700bd4eb7b7ab12bc0850950bfe6e69edd6ac1daa5db2c2b07484327196e561c513462d72872dc6771c39f6b60d46a1f2c92343b7338450a0ef8e39f97fa70652b3a12cd04043698951627aaaa82cc95e76df92021d30e8014c984f12eea0143de8b17e5e4a36ec07bf4814251b391f168a59ef75afcd2319249aaba930f06bb7a11b9491e6f71b3d5774a6503a965e94edd0a67737282fc9cb0271779ff14151b7aa9267bb8f7d643185512515aeea513c0c98bfae782381a3317064195d8825cf8b25c17cdab5fced02612a3f2870e40df57e6ca3f08228a2b04e8de1425eb4b970118f9bbdc212223ff86a5d6b648cdf2366722f21de4b14a1014879eadb69215cdb1aa2a9f4f310ecfe3116214fe3ab0a23f4775a0a54b48d7dfd8f7283ed687b3ac7e1a7e42a0bdc3478aba8651c03e1e9cc9df17d106b8130afe854269b0103b7a696f452721887b19d8181830073c9f10684c65f96d3a6c6efbae044eec03d6399e001fa44d54635dc72f9b8ea6b87d0f452cad1e1e32273e2b47c40f2730235adcae8523b8282f86b8cf1ab63ae54aaa06130df3bbf6ecac7d7d1d43d2a87aea837267ff8ccfaa4b7e47b7ded909e6603d0b928a304f8915c839153598adc4178eb48bc0e98ad7793d7980275e1e491ba4847a4a04ae30fe7f5cc7d4b6f4f63a525e9964d72245860ca76a668a4654adb6619f16e9db79131e5675b93cafb96c92f1da8464d4fef2a22e7f9db695965fe2cc27ea30974629c8fe17cfa2f860179e1eb9faaa88a91ec9ce6da28c1a2894c3b932b5e1c807146718cc77ca13c61eaae00c7c99e019f599772064b198c5c2c5e863336367673630b417ac845ddb7c93b0856317e5d64bab208c5730abc2c63536784fbeaaec139dffc917e775715f1e42164ddef5138d4d163609ab3fbdcab968f8738385c0e7e34ff3cf7771a1dc5ba25a8850fdf96dabafa21f9065f307457ce9af4b7a73450c9d20a3b46fa8d3a1163d22bd01a7d17f0ec274181bf9640fa941427694bfeb1346089f7a851efe0fbb7a2041fa6bb6541ccbad77dd3e1a97999fc05f1fef070e7b5c4b385b8b2a8cc32483fdeba6a373970de2fa4139ba18e5916f949aab0aab2894")
require.NoError(t, err)
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
require.ErrorIs(t, err, sniff.ErrClientHelloFragmented)
require.ErrorIs(t, err, sniff.ErrNeedMoreData)
pkt, err = hex.DecodeString("c20000000108e241a0c601413b4f004046006d8f15dae9999edf39d58df6762822b9a2ab996d7f6a10044338af3b51b1814bc4ac0fa5a87c34c6ae604af8cabc5957c5240174deefc8e378719ffdab2ae4e15bf4514bea4489e2ff30c43a5f63beb2e4501ce7754085bcbe838003a0b4bccb53863c0766df7eac073c2bdc170772b157997945acdc2ab2e84750cc9aa0ffa0fdc023da7fc565a14f87f7c563dbc9183dd226aab79957d263f66e64b85a1b15a24516bd2c7c04eea4fa0a34ef9849c21585db2e4adb7c05e265c4f38d8ffe4cbed0f3b0e68f3693bf1f726c3fb135b8e32a5d22931d7c55fc2ff4b9a354933ab14544df3cdaf3e3217dfb8d7feb3465dc34df6320ea486f12e5b2d609aaa5f4515c20c86fc440f8087be0ee3d339835746ae2573c2afdee6bb6ef7e9eb541feae9209391b2902cfb0bdaccd9da8d290714638b7da588d4a656ca6eabba78b7363922d6037cf060b161a42019d4feb4156459103cffdeefd0e63114af2b0e0c39e70ebc7fecb8dd1ebb8d60b2137f509bb7dcef5f1d3e06ab1d391466652d57440a410fb4f58a6ce1fb62feb453241f64e110709f59a3d9ebdac94f811337d0e4a80fd6b56b2a70cd6eebbf98e1661291da6bf5beb8b8afc376dfd20eb76afe709e8e8f28e0ef82105954e346546ad25973df43f4acddbec0ffd9b215f62abebebf71305b5ea993560316f69430bf5afe50420340622f802b5830f3bcebffff04980c75a59d28902879e5d51a4fb21062a4ae13c42297075b21d54ee04303879c1157e7470c1451673c98a2f3921f2f3e8f6acfe85b01caaca66b59e5ebffbfe68e5e9ab17e9a1b857eb409df91cb76767fc1814fd3c522a9b117edd0b02526e469cb4afb291a4dcc74c79b47ec6e7ce558c597129366f83ec306b11d2598c705fd4ee9ee99df6b7039bef13b08fc6f26853ad213829d24f895747d45a47414f931c583fb6c3e4f6c27d0c2b81a5f3cee390ec6314e1fec637e8d28b675e97caafdfbf8c25d34a635083a7553d219dd80dbb39087d74c6ad6192ca6f48a3ff8d47db41b2a492c63fcd780012780931dae0a325f9dcbd772d09a700f132c4bc1d9809b25b9751b694eb72a8ba4db7208d2b1bab63e1845208e4f841ea30218a559db98751589716b6d059ca673378f5fe7c7d8a1c82e14a561c47313bbcc278412ba86ffb2b87ec308eab9df696f5b4b54f8e361731bf232820a02a35fda7e5d4bf01b8f005ad299a055116e7b23c181f15a66442cf6032ca477bccc55b79d424eb4f245847bd81a581dc369dd20b1a4892733bde3c38e492c0039f69f2b947a4dc251a49ee7ccc0f36b3b75a555fa1d126db75f94dab60f52f6b15a877a0c380b59f82d35c570bc5f8051e9ef87db51f52383d47b50829b7f9e947ccc67aa280566aa48b4a85c1c7eca6f542789d8abcc050f1aa3cc221b6859656a21454aa21c7bfb9d12115f61c3ed46263ade68a8d3679fa62a659a5da7817406bd16618fccf33ed208ada1b03584e8b485d3cb6ed80a0774e60b6cd55aff64169ea998cf8235997049515abac58e0169ca07fb1c8c4c8b2803ba9d27b44c045d0a1cac86e5e188195c68001f53eb44851b6d821fc01ccbb41e27f38e6ddd66540c2d62ed6e0d551e22c0f26b60078c74a6302a1ed3d9e8fc0861257a63f6ac4e759fd54bff088becd28e30944a6c15db4fc8ae6244346869add946d9d92c430d737e042fa18b28a8ed64d1e8987ad9061cdc1335f")
require.NoError(t, err)
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
@@ -40,7 +40,7 @@ func TestSniffQUICChromium(t *testing.T) {
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
require.Equal(t, metadata.Protocol, C.ProtocolQUIC)
require.Equal(t, metadata.Client, C.ClientChromium)
require.ErrorIs(t, err, sniff.ErrClientHelloFragmented)
require.ErrorIs(t, err, sniff.ErrNeedMoreData)
pkt, err = hex.DecodeString("c90000000108f40d654cc09b27f5000044d073eb38807026d4088455e650e7ccf750d01a72f15f9bfc8ff40d223499db1a485cff14dbd45b9be118172834dc35dca3cf62f61a1266f40b92faf3d28d67a466cfdca678ddced15cd606d31959cf441828467857b226d1a241847c82c57312cefe68ba5042d929919bcd4403b39e5699fe87dda05df1b3801e048edee792458e9b1a9b1d4039df05847bcee3be567494b5876e3bd4c3220fe9dfdb2c07d77410f907f744251ef15536cc03b267d3668d5b75bc1ad2fe735cd3bb73519dd9f1625a49e17ad27bdeccf706c83b5ea339a0a05dd0072f4a8f162bd29926b4997f05613c6e4b0270b0c02805ca0543f27c1ff8505a5750bdd33529ee73c491050a10c6903f53c1121dbe0380e84c007c8df74a1b02443ed80ba7766aef5549e618d4fd249844ee28565142005369869299e8c3035ecef3d799f6cada8549e75b4ce4cbf4c85ef071fd7ff067b1ca9b5968dc41d13d011f6d7843823bac97acb1eb8ee45883f0f254b5f9bd4c763b67e2d8c70a7618a0ef0de304cf597a485126e09f8b2fd795b394c0b4bc4cd2634c2057970da2c798c5e8af7aed4f76f5e25d04e3f8c9c5a5b150d17e0d4c74229898c69b8dc7b8bcc9d359eb441de75c68fbdebec62fb669dcccfb1aad03e3fa073adb2ccf7bb14cbaf99e307d2c903ee71a8f028102eb510caee7e7397512086a78d1f95635c7d06845b5a708652dc4e5cd61245aae5b3c05b84815d84d367bce9b9e3f6d6b90701ac3679233c14d5ce2a1eff26469c966266dc6284bdb95c9c6158934c413a872ce22101e4163e3293d236b301592ca4ccacc1fd4c37066e79c2d9857c8a2560dcf0b33b19163c4240c471b19907476e7e25c65f7eb37276594a0f6b4c33c340cc3284178f17ac5e34dbe7509db890e4ddfd0540fbf9deb32a0101d24fe58b26c5f81c627db9d6ae59d7a111a3d5d1f6109f4eec0d0234e6d73c73a44f50999462724b51ce0fd8283535d70d9e83872c79c59897407a0736741011ae5c64862eb0712f9e7b07aa1d5418ca3fde8626257c6fe418f3c5479055bb2b0ab4c25f649923fc2a41c79aaa7d0f3af6d8b8cf06f61f0230d09bbb60bb49b9e49cc5973748a6cf7ffdee7804d424f9423c63e7ff22f4bd24e4867636ef9fe8dd37f59941a8a47c27765caa8e875a30b62834f17c569227e5e6ed15d58e05d36e76332befad065a2cd4079e66d5af189b0337624c89b1560c3b1b0befd5c1f20e6de8e3d664b3ac06b3d154b488983e14aa93266f5f8b621d2a9bb7ccce509eb26e025c9c45f7cccc09ce85b3103af0c93ce9822f82ecb168ca3177829afb2ea0da2c380e7b1728add55a5d42632e2290363d4cbe432b67e13691648e1acfab22cf0d551eee857709b428bb78e27a45aff6eca301c02e4d13cf36cc2494fdd1aef8dede6e18febd79dca4c6964d09b91c25a08f0947c76ab5104de9404459c2edf5f4adb9dfd771be83656f77fbbafb1ad3281717066010be8778952495383c9f2cf0a38527228c662a35171c5981731f1af09bab842fe6c3162ad4152a4221f560eb6f9bea66b294ffbd3643da2fe34096da13c246505452540177a2a0a1a69106e5cfc279a4890fc3be2952f26be245f930e6c2d9e7e26ee960481e72b99594a1185b46b94b6436d00ba6c70ffe135d43907c92c6f1c09fb9453f103730714f5700fa4347f9715c774cb04a7218dacc66d9c2fade18b14e684aa7fc9ebda0a28")
require.NoError(t, err)
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)

View File

@@ -8,6 +8,7 @@ import (
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/rw"
)
@@ -15,7 +16,7 @@ func RDP(_ context.Context, metadata *adapter.InboundContext, reader io.Reader)
var tpktVersion uint8
err := binary.Read(reader, binary.BigEndian, &tpktVersion)
if err != nil {
return err
return E.Cause1(ErrNeedMoreData, err)
}
if tpktVersion != 0x03 {
return os.ErrInvalid
@@ -24,7 +25,7 @@ func RDP(_ context.Context, metadata *adapter.InboundContext, reader io.Reader)
var tpktReserved uint8
err = binary.Read(reader, binary.BigEndian, &tpktReserved)
if err != nil {
return err
return E.Cause1(ErrNeedMoreData, err)
}
if tpktReserved != 0x00 {
return os.ErrInvalid
@@ -33,7 +34,7 @@ func RDP(_ context.Context, metadata *adapter.InboundContext, reader io.Reader)
var tpktLength uint16
err = binary.Read(reader, binary.BigEndian, &tpktLength)
if err != nil {
return err
return E.Cause1(ErrNeedMoreData, err)
}
if tpktLength != 19 {
@@ -43,7 +44,7 @@ func RDP(_ context.Context, metadata *adapter.InboundContext, reader io.Reader)
var cotpLength uint8
err = binary.Read(reader, binary.BigEndian, &cotpLength)
if err != nil {
return err
return E.Cause1(ErrNeedMoreData, err)
}
if cotpLength != 14 {
@@ -53,7 +54,7 @@ func RDP(_ context.Context, metadata *adapter.InboundContext, reader io.Reader)
var cotpTpduType uint8
err = binary.Read(reader, binary.BigEndian, &cotpTpduType)
if err != nil {
return err
return E.Cause1(ErrNeedMoreData, err)
}
if cotpTpduType != 0xE0 {
return os.ErrInvalid
@@ -61,13 +62,13 @@ func RDP(_ context.Context, metadata *adapter.InboundContext, reader io.Reader)
err = rw.SkipN(reader, 5)
if err != nil {
return err
return E.Cause1(ErrNeedMoreData, err)
}
var rdpType uint8
err = binary.Read(reader, binary.BigEndian, &rdpType)
if err != nil {
return err
return E.Cause1(ErrNeedMoreData, err)
}
if rdpType != 0x01 {
return os.ErrInvalid
@@ -75,12 +76,12 @@ func RDP(_ context.Context, metadata *adapter.InboundContext, reader io.Reader)
var rdpFlags uint8
err = binary.Read(reader, binary.BigEndian, &rdpFlags)
if err != nil {
return err
return E.Cause1(ErrNeedMoreData, err)
}
var rdpLength uint8
err = binary.Read(reader, binary.BigEndian, &rdpLength)
if err != nil {
return err
return E.Cause1(ErrNeedMoreData, err)
}
if rdpLength != 8 {
return os.ErrInvalid

View File

@@ -3,6 +3,7 @@ package sniff
import (
"bytes"
"context"
"errors"
"io"
"net"
"time"
@@ -19,6 +20,8 @@ type (
PacketSniffer = func(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error
)
var ErrNeedMoreData = E.New("need more data")
func Skip(metadata *adapter.InboundContext) bool {
// skip server first protocols
switch metadata.Destination.Port {
@@ -40,7 +43,7 @@ func PeekStream(ctx context.Context, metadata *adapter.InboundContext, conn net.
timeout = C.ReadPayloadTimeout
}
deadline := time.Now().Add(timeout)
var errors []error
var sniffError error
for i := 0; ; i++ {
err := conn.SetReadDeadline(deadline)
if err != nil {
@@ -54,7 +57,7 @@ func PeekStream(ctx context.Context, metadata *adapter.InboundContext, conn net.
}
return E.Cause(err, "read payload")
}
errors = nil
sniffError = nil
for _, sniffer := range sniffers {
reader := io.MultiReader(common.Map(append(buffers, buffer), func(it *buf.Buffer) io.Reader {
return bytes.NewReader(it.Bytes())
@@ -63,20 +66,23 @@ func PeekStream(ctx context.Context, metadata *adapter.InboundContext, conn net.
if err == nil {
return nil
}
errors = append(errors, err)
sniffError = E.Errors(sniffError, err)
}
if !errors.Is(err, ErrNeedMoreData) {
break
}
}
return E.Errors(errors...)
return sniffError
}
func PeekPacket(ctx context.Context, metadata *adapter.InboundContext, packet []byte, sniffers ...PacketSniffer) error {
var errors []error
var sniffError []error
for _, sniffer := range sniffers {
err := sniffer(ctx, metadata, packet)
if err == nil {
return nil
}
errors = append(errors, err)
sniffError = append(sniffError, err)
}
return E.Errors(errors...)
return E.Errors(sniffError...)
}

View File

@@ -5,22 +5,26 @@ import (
"context"
"io"
"os"
"strings"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
E "github.com/sagernet/sing/common/exceptions"
)
func SSH(_ context.Context, metadata *adapter.InboundContext, reader io.Reader) error {
scanner := bufio.NewScanner(reader)
if !scanner.Scan() {
const sshPrefix = "SSH-2.0-"
bReader := bufio.NewReader(reader)
prefix, err := bReader.Peek(len(sshPrefix))
if err != nil {
return E.Cause1(ErrNeedMoreData, err)
} else if string(prefix) != sshPrefix {
return os.ErrInvalid
}
fistLine := scanner.Text()
if !strings.HasPrefix(fistLine, "SSH-2.0-") {
return os.ErrInvalid
fistLine, _, err := bReader.ReadLine()
if err != nil {
return err
}
metadata.Protocol = C.ProtocolSSH
metadata.Client = fistLine[8:]
metadata.Client = string(fistLine)[8:]
return nil
}

View File

@@ -3,11 +3,13 @@ package sniff
import (
"context"
"crypto/tls"
"errors"
"io"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing/common/bufio"
E "github.com/sagernet/sing/common/exceptions"
)
func TLSClientHello(ctx context.Context, metadata *adapter.InboundContext, reader io.Reader) error {
@@ -21,8 +23,11 @@ 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
if errors.Is(err, io.ErrUnexpectedEOF) {
return E.Cause1(ErrNeedMoreData, err)
} else {
return err
}
}

View File

@@ -37,7 +37,7 @@ func (w *acmeWrapper) Close() error {
return nil
}
func startACME(ctx context.Context, options option.InboundACMEOptions) (*tls.Config, adapter.Service, error) {
func startACME(ctx context.Context, options option.InboundACMEOptions) (*tls.Config, adapter.SimpleLifecycle, error) {
var acmeServer string
switch options.Provider {
case "", "letsencrypt":

View File

@@ -11,6 +11,6 @@ import (
E "github.com/sagernet/sing/common/exceptions"
)
func startACME(ctx context.Context, options option.InboundACMEOptions) (*tls.Config, adapter.Service, error) {
func startACME(ctx context.Context, options option.InboundACMEOptions) (*tls.Config, adapter.SimpleLifecycle, error) {
return nil, nil, E.New(`ACME is not included in this build, rebuild with -tags with_acme`)
}

View File

@@ -123,6 +123,7 @@ func (s *STDECHClientConfig) ClientHandshake(ctx context.Context, conn net.Conn)
if response.Rcode != mDNS.RcodeSuccess {
return nil, E.Cause(dns.RcodeError(response.Rcode), "fetch ECH config list")
}
match:
for _, rr := range response.Answer {
switch resource := rr.(type) {
case *mDNS.HTTPS:
@@ -133,11 +134,14 @@ func (s *STDECHClientConfig) ClientHandshake(ctx context.Context, conn net.Conn)
return nil, E.Cause(err, "decode ECH config")
}
s.config.EncryptedClientHelloConfigList = echConfigList
break match
}
}
}
}
return nil, E.New("no ECH config found in DNS records")
if len(s.config.EncryptedClientHelloConfigList) == 0 {
return nil, E.New("no ECH config found in DNS records")
}
}
tlsConn, err := s.Client(conn)
if err != nil {

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

@@ -89,16 +89,20 @@ func NewRealityServer(ctx context.Context, logger log.Logger, options option.Inb
tlsConfig.MaxTimeDiff = time.Duration(options.Reality.MaxTimeDifference)
tlsConfig.ShortIds = make(map[[8]byte]bool)
for i, shortIDString := range options.Reality.ShortID {
var shortID [8]byte
decodedLen, err := hex.Decode(shortID[:], []byte(shortIDString))
if err != nil {
return nil, E.Cause(err, "decode short_id[", i, "]: ", shortIDString)
if len(options.Reality.ShortID) == 0 {
tlsConfig.ShortIds[[8]byte{0}] = true
} else {
for i, shortIDString := range options.Reality.ShortID {
var shortID [8]byte
decodedLen, err := hex.Decode(shortID[:], []byte(shortIDString))
if err != nil {
return nil, E.Cause(err, "decode short_id[", i, "]: ", shortIDString)
}
if decodedLen > 8 {
return nil, E.New("invalid short_id[", i, "]: ", shortIDString)
}
tlsConfig.ShortIds[shortID] = true
}
if decodedLen > 8 {
return nil, E.New("invalid short_id[", i, "]: ", shortIDString)
}
tlsConfig.ShortIds[shortID] = true
}
handshakeDialer, err := dialer.New(ctx, options.Reality.Handshake.DialerOptions, options.Reality.Handshake.ServerIsDomain())

View File

@@ -6,6 +6,7 @@ import (
"net"
"os"
"strings"
"time"
"github.com/sagernet/fswatch"
"github.com/sagernet/sing-box/adapter"
@@ -21,7 +22,7 @@ var errInsecureUnused = E.New("tls: insecure unused")
type STDServerConfig struct {
config *tls.Config
logger log.Logger
acmeService adapter.Service
acmeService adapter.SimpleLifecycle
certificate []byte
key []byte
certificatePath string
@@ -164,7 +165,7 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound
return nil, nil
}
var tlsConfig *tls.Config
var acmeService adapter.Service
var acmeService adapter.SimpleLifecycle
var err error
if options.ACME != nil && len(options.ACME.Domain) > 0 {
//nolint:staticcheck
@@ -233,8 +234,12 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound
key = content
}
if certificate == nil && key == nil && options.Insecure {
timeFunc := ntp.TimeFuncFromContext(ctx)
if timeFunc == nil {
timeFunc = time.Now
}
tlsConfig.GetCertificate = func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
return GenerateKeyPair(nil, nil, ntp.TimeFuncFromContext(ctx), info.ServerName)
return GenerateKeyPair(nil, nil, timeFunc, info.ServerName)
}
} else {
if certificate == nil {

View File

@@ -25,6 +25,10 @@ const (
TypeTUIC = "tuic"
TypeHysteria2 = "hysteria2"
TypeTailscale = "tailscale"
TypeDERP = "derp"
TypeDERPSTUN = "derp-stun"
TypeResolved = "resolved"
TypeSSMAPI = "ssm-api"
)
const (

View File

@@ -243,9 +243,15 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error) {
domain = FqdnToDomain(domain)
dnsName := dns.Fqdn(domain)
if options.Strategy == C.DomainStrategyIPv4Only {
var strategy C.DomainStrategy
if options.LookupStrategy != C.DomainStrategyAsIS {
strategy = options.LookupStrategy
} else {
strategy = options.Strategy
}
if strategy == C.DomainStrategyIPv4Only {
return c.lookupToExchange(ctx, transport, dnsName, dns.TypeA, options, responseChecker)
} else if options.Strategy == C.DomainStrategyIPv6Only {
} else if strategy == C.DomainStrategyIPv6Only {
return c.lookupToExchange(ctx, transport, dnsName, dns.TypeAAAA, options, responseChecker)
}
var response4 []netip.Addr
@@ -271,7 +277,7 @@ func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, dom
if len(response4) == 0 && len(response6) == 0 {
return nil, err
}
return sortAddresses(response4, response6, options.Strategy), nil
return sortAddresses(response4, response6, strategy), nil
}
func (c *Client) ClearCache() {
@@ -527,12 +533,26 @@ func transportTagFromContext(ctx context.Context) (string, bool) {
return value, loaded
}
func FixedResponseStatus(message *dns.Msg, rcode int) *dns.Msg {
return &dns.Msg{
MsgHdr: dns.MsgHdr{
Id: message.Id,
Rcode: rcode,
Response: true,
},
Question: message.Question,
}
}
func FixedResponse(id uint16, question dns.Question, addresses []netip.Addr, timeToLive uint32) *dns.Msg {
response := dns.Msg{
MsgHdr: dns.MsgHdr{
Id: id,
Rcode: dns.RcodeSuccess,
Response: true,
Id: id,
Response: true,
Authoritative: true,
RecursionDesired: true,
RecursionAvailable: true,
Rcode: dns.RcodeSuccess,
},
Question: []dns.Question{question},
}
@@ -565,9 +585,12 @@ func FixedResponse(id uint16, question dns.Question, addresses []netip.Addr, tim
func FixedResponseCNAME(id uint16, question dns.Question, record string, timeToLive uint32) *dns.Msg {
response := dns.Msg{
MsgHdr: dns.MsgHdr{
Id: id,
Rcode: dns.RcodeSuccess,
Response: true,
Id: id,
Response: true,
Authoritative: true,
RecursionDesired: true,
RecursionAvailable: true,
Rcode: dns.RcodeSuccess,
},
Question: []dns.Question{question},
Answer: []dns.RR{
@@ -588,9 +611,12 @@ func FixedResponseCNAME(id uint16, question dns.Question, record string, timeToL
func FixedResponseTXT(id uint16, question dns.Question, records []string, timeToLive uint32) *dns.Msg {
response := dns.Msg{
MsgHdr: dns.MsgHdr{
Id: id,
Rcode: dns.RcodeSuccess,
Response: true,
Id: id,
Response: true,
Authoritative: true,
RecursionDesired: true,
RecursionAvailable: true,
Rcode: dns.RcodeSuccess,
},
Question: []dns.Question{question},
Answer: []dns.RR{
@@ -611,9 +637,12 @@ func FixedResponseTXT(id uint16, question dns.Question, records []string, timeTo
func FixedResponseMX(id uint16, question dns.Question, records []*net.MX, timeToLive uint32) *dns.Msg {
response := dns.Msg{
MsgHdr: dns.MsgHdr{
Id: id,
Rcode: dns.RcodeSuccess,
Response: true,
Id: id,
Response: true,
Authoritative: true,
RecursionDesired: true,
RecursionAvailable: true,
Rcode: dns.RcodeSuccess,
},
Question: []dns.Question{question},
}

View File

@@ -15,6 +15,8 @@ func TruncateDNSMessage(request *dns.Msg, response *dns.Msg, headroom int) (*buf
}
responseLen := response.Len()
if responseLen > maxLen {
copyResponse := *response
response = &copyResponse
response.Truncate(maxLen)
}
buffer := buf.NewSize(headroom*2 + 1 + responseLen)

View File

@@ -285,7 +285,12 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte
} else if errors.Is(err, ErrResponseRejected) {
rejected = true
r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())))
/*} else if responseCheck!= nil && errors.Is(err, RcodeError(mDNS.RcodeNameError)) {
rejected = true
r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())))
*/
} else if len(message.Question) > 0 {
rejected = true
r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for ", FormatQuestion(message.Question[0].String())))
} else {
r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for <empty query>"))

View File

@@ -35,6 +35,7 @@ func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, opt
}
return &Transport{
TransportAdapter: dns.NewTransportAdapterWithLocalOptions(C.DNSTypeLocal, tag, options),
ctx: ctx,
hosts: hosts.NewFile(hosts.DefaultPath),
dialer: transportDialer,
}, nil
@@ -57,7 +58,7 @@ func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg,
return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil
}
}
systemConfig := getSystemDNSConfig()
systemConfig := getSystemDNSConfig(t.ctx)
if systemConfig.singleRequest || !(message.Question[0].Qtype == mDNS.TypeA || message.Question[0].Qtype == mDNS.TypeAAAA) {
return t.exchangeSingleRequest(ctx, systemConfig, message, domain)
} else {

View File

@@ -1,6 +1,7 @@
package local
import (
"context"
"os"
"runtime"
"strings"
@@ -23,19 +24,21 @@ type resolverConfig struct {
var resolvConf resolverConfig
func getSystemDNSConfig() *dnsConfig {
resolvConf.tryUpdate("/etc/resolv.conf")
func getSystemDNSConfig(ctx context.Context) *dnsConfig {
resolvConf.tryUpdate(ctx, "/etc/resolv.conf")
return resolvConf.dnsConfig.Load()
}
func (conf *resolverConfig) init() {
conf.dnsConfig.Store(dnsReadConfig("/etc/resolv.conf"))
func (conf *resolverConfig) init(ctx context.Context) {
conf.dnsConfig.Store(dnsReadConfig(ctx, "/etc/resolv.conf"))
conf.lastChecked = time.Now()
conf.ch = make(chan struct{}, 1)
}
func (conf *resolverConfig) tryUpdate(name string) {
conf.initOnce.Do(conf.init)
func (conf *resolverConfig) tryUpdate(ctx context.Context, name string) {
conf.initOnce.Do(func() {
conf.init(ctx)
})
if conf.dnsConfig.Load().noReload {
return
@@ -59,7 +62,7 @@ func (conf *resolverConfig) tryUpdate(name string) {
return
}
}
dnsConf := dnsReadConfig(name)
dnsConf := dnsReadConfig(ctx, name)
conf.dnsConfig.Store(dnsConf)
}

View File

@@ -11,6 +11,7 @@ package local
import "C"
import (
"context"
"time"
E "github.com/sagernet/sing/common/exceptions"
@@ -18,7 +19,7 @@ import (
"github.com/miekg/dns"
)
func dnsReadConfig(_ string) *dnsConfig {
func dnsReadConfig(_ context.Context, _ string) *dnsConfig {
if C.res_init() != 0 {
return &dnsConfig{
servers: defaultNS,

View File

@@ -4,6 +4,7 @@ package local
import (
"bufio"
"context"
"net"
"net/netip"
"os"
@@ -13,7 +14,7 @@ import (
"github.com/miekg/dns"
)
func dnsReadConfig(name string) *dnsConfig {
func dnsReadConfig(_ context.Context, name string) *dnsConfig {
conf := &dnsConfig{
ndots: 1,
timeout: 5 * time.Second,

View File

@@ -1,6 +1,7 @@
package local
import (
"context"
"net"
"net/netip"
"os"
@@ -8,10 +9,13 @@ import (
"time"
"unsafe"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing/service"
"golang.org/x/sys/windows"
)
func dnsReadConfig(_ string) *dnsConfig {
func dnsReadConfig(ctx context.Context, _ string) *dnsConfig {
conf := &dnsConfig{
ndots: 1,
timeout: 5 * time.Second,
@@ -22,35 +26,35 @@ func dnsReadConfig(_ string) *dnsConfig {
conf.servers = defaultNS
}
}()
aas, err := adapterAddresses()
addresses, err := adapterAddresses()
if err != nil {
return nil
}
for _, aa := range aas {
// Only take interfaces whose OperStatus is IfOperStatusUp(0x01) into DNS configs.
if aa.OperStatus != windows.IfOperStatusUp {
var dnsAddresses []struct {
ifName string
netip.Addr
}
for _, address := range addresses {
if address.OperStatus != windows.IfOperStatusUp {
continue
}
// Only take interfaces which have at least one gateway
if aa.FirstGatewayAddress == nil {
if address.IfType == windows.IF_TYPE_TUNNEL {
continue
}
for dns := aa.FirstDnsServerAddress; dns != nil; dns = dns.Next {
sa, err := dns.Address.Sockaddr.Sockaddr()
if address.FirstGatewayAddress == nil {
continue
}
for dnsServerAddress := address.FirstDnsServerAddress; dnsServerAddress != nil; dnsServerAddress = dnsServerAddress.Next {
rawSockaddr, err := dnsServerAddress.Address.Sockaddr.Sockaddr()
if err != nil {
continue
}
var ip netip.Addr
switch sa := sa.(type) {
var dnsServerAddr netip.Addr
switch sockaddr := rawSockaddr.(type) {
case *syscall.SockaddrInet4:
ip = netip.AddrFrom4([4]byte{sa.Addr[0], sa.Addr[1], sa.Addr[2], sa.Addr[3]})
dnsServerAddr = netip.AddrFrom4(sockaddr.Addr)
case *syscall.SockaddrInet6:
var addr16 [16]byte
copy(addr16[:], sa.Addr[:])
if addr16[0] == 0xfe && addr16[1] == 0xc0 {
if sockaddr.Addr[0] == 0xfe && sockaddr.Addr[1] == 0xc0 {
// fec0/10 IPv6 addresses are site local anycast DNS
// addresses Microsoft sets by default if no other
// IPv6 DNS address is set. Site local anycast is
@@ -58,14 +62,27 @@ func dnsReadConfig(_ string) *dnsConfig {
// https://datatracker.ietf.org/doc/html/rfc3879
continue
}
ip = netip.AddrFrom16(addr16)
dnsServerAddr = netip.AddrFrom16(sockaddr.Addr)
default:
// Unexpected type.
continue
}
conf.servers = append(conf.servers, net.JoinHostPort(ip.String(), "53"))
dnsAddresses = append(dnsAddresses, struct {
ifName string
netip.Addr
}{ifName: windows.UTF16PtrToString(address.FriendlyName), Addr: dnsServerAddr})
}
}
var myInterface string
if networkManager := service.FromContext[adapter.NetworkManager](ctx); networkManager != nil {
myInterface = networkManager.InterfaceMonitor().MyInterface()
}
for _, address := range dnsAddresses {
if address.ifName == myInterface {
continue
}
conf.servers = append(conf.servers, net.JoinHostPort(address.String(), "53"))
}
return conf
}

View File

@@ -140,12 +140,12 @@ func (t *Transport) exchange(ctx context.Context, message *mDNS.Msg, conn quic.C
if err != nil {
return nil, err
}
defer stream.Close()
defer stream.CancelRead(0)
err = transport.WriteMessage(stream, 0, message)
if err != nil {
stream.Close()
return nil, err
}
stream.Close()
return transport.ReadMessage(stream)
}

View File

@@ -57,13 +57,17 @@ func NewTLS(ctx context.Context, logger log.ContextLogger, tag string, options o
if serverAddr.Port == 0 {
serverAddr.Port = 853
}
return NewTLSRaw(logger, dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeTLS, tag, options.RemoteDNSServerOptions), transportDialer, serverAddr, tlsConfig), nil
}
func NewTLSRaw(logger logger.ContextLogger, adapter dns.TransportAdapter, dialer N.Dialer, serverAddr M.Socksaddr, tlsConfig tls.Config) *TLSTransport {
return &TLSTransport{
TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeTLS, tag, options.RemoteDNSServerOptions),
TransportAdapter: adapter,
logger: logger,
dialer: transportDialer,
dialer: dialer,
serverAddr: serverAddr,
tlsConfig: tlsConfig,
}, nil
}
}
func (t *TLSTransport) Start(stage adapter.StartStage) error {

View File

@@ -212,8 +212,8 @@ type dnsConnection struct {
func (c *dnsConnection) Close(err error) {
c.closeOnce.Do(func() {
close(c.done)
c.err = err
close(c.done)
})
c.Conn.Close()
}

View File

@@ -2,10 +2,31 @@
icon: material/alert-decagram
---
#### 1.12.0-alpha.20
#### 1.12.0-beta.3
* Fixes and improvements
### 1.11.7
* Fixes and improvements
_We are temporarily unable to update sing-box apps on the App Store because the reviewer mistakenly found that we violated the rules (TestFlight users are not affected)._
#### 1.12.0-beta.1
* Fixes and improvements
**1**:
Now `auto_redirect` fixes compatibility issues between tun and Docker bridge networks,
see [Tun](/configuration/inbound/tun/#auto_redirect).
### 1.11.6
* Fixes and improvements
_We are temporarily unable to update sing-box apps on the App Store because the reviewer mistakenly found that we violated the rules (TestFlight users are not affected)._
#### 1.12.0-alpha.19
* Update gVisor to 20250319.0

View File

@@ -44,10 +44,10 @@ Default padding scheme:
```
stop=8
0=34-120
0=30-30
1=100-400
2=400-500,c,500-1000,c,400-500,c,500-1000,c,500-1000,c,400-500
3=500-1000
2=400-500,c,500-1000,c,500-1000,c,500-1000,c,500-1000
3=9-9,500-1000
4=500-1000
5=500-1000
6=500-1000

View File

@@ -44,10 +44,10 @@ AnyTLS 填充方案行数组。
```
stop=8
0=34-120
0=30-30
1=100-400
2=400-500,c,500-1000,c,400-500,c,500-1000,c,500-1000,c,400-500
3=500-1000
2=400-500,c,500-1000,c,500-1000,c,500-1000,c,500-1000
3=9-9,500-1000
4=500-1000
5=500-1000
6=500-1000

View File

@@ -211,6 +211,10 @@ Set the default route to the Tun.
By default, VPN takes precedence over tun. To make tun go through VPN, enable `route.override_android_vpn`.
!!! note "Also enable `auto_redirect`"
`auto_redirect` is always recommended on Linux, it provides better routing, higher performance (better than tproxy), and avoids conflicts between TUN and Docker bridge networks.
#### iproute2_table_index
!!! question "Since sing-box 1.10.0"
@@ -235,22 +239,29 @@ Linux iproute2 rule start index generated by `auto_route`.
Only supported on Linux with `auto_route` enabled.
Automatically configure iptables/nftables to redirect connections.
Improve TUN routing and performance using nftables.
*In Android*
`auto_redirect` is always recommended on Linux, it provides better routing,
higher performance (better than tproxy),
and avoids conflicts between TUN and Docker bridge networks.
Only local IPv4 connections are forwarded. To share your VPN connection over hotspot or repeater,
Note that `auto_redirect` also works on Android,
but due to the lack of `nftables` and `ip6tables`,
only simple IPv4 TCP forwarding is performed.
To share your VPN connection over hotspot or repeater on Android,
use [VPNHotspot](https://github.com/Mygod/VPNHotspot).
*In Linux*:
`auto_redirect` also automatically inserts compatibility rules
into the OpenWrt fw4 table, i.e.
it will work on routers without any extra configuration.
`auto_route` with `auto_redirect` works as expected on routers **without intervention**.
Conflict with `route.default_mark` and `[dialOptions].routing_mark`.
#### auto_redirect_input_mark
!!! question "Since sing-box 1.10.0"
Connection input mark used by `route[_exclude]_address_set` with `auto_redirect`.
Connection input mark used by `auto_redirect`.
`0x2023` is used by default.
@@ -258,7 +269,7 @@ Connection input mark used by `route[_exclude]_address_set` with `auto_redirect`
!!! question "Since sing-box 1.10.0"
Connection input mark used by `route[_exclude]_address_set` with `auto_redirect`.
Connection output mark used by `auto_redirect`.
`0x2024` is used by default.
@@ -269,17 +280,15 @@ Enforce strict routing rules when `auto_route` is enabled:
*In Linux*:
* Let unsupported network unreachable
* Make ICMP traffic route to tun instead of upstream interfaces
* Route all connections to tun
It prevents IP address leaks and makes DNS hijacking work on Android.
* For legacy reasons, when neither `strict_route` nor `auto_redirect` are enabled, all ICMP traffic will not go through TUN.
*In Windows*:
* Add firewall rules to prevent DNS leak caused by
* Let unsupported network unreachable
* prevent DNS leak caused by
Windows' [ordinary multihomed DNS resolution behavior](https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2008-R2-and-2008/dd197552%28v%3Dws.10%29)
It may prevent some applications (such as VirtualBox) from working properly in certain situations.
It may prevent some Windows applications (such as VirtualBox) from working properly in certain situations.
#### route_address
@@ -367,8 +376,6 @@ Exclude custom routes when `auto_route` is enabled.
Add the destination IP CIDR rules in the specified rule-sets to the firewall.
Matched traffic will bypass the sing-box routes.
Conflict with `route.default_mark` and `[dialOptions].routing_mark`.
=== "Without `auto_redirect` enabled"

View File

@@ -215,6 +215,10 @@ tun 接口的 IPv6 前缀。
VPN 默认优先于 tun。要使 tun 经过 VPN启用 `route.override_android_vpn`
!!! note "也启用 `auto_redirect`"
在 Linux 上始终推荐使用 `auto_redirect`,它提供更好的路由, 更高的性能(优于 tproxy 并避免 TUN 与 Docker 桥接网络冲突。
#### iproute2_table_index
!!! question "自 sing-box 1.10.0 起"
@@ -237,23 +241,24 @@ tun 接口的 IPv6 前缀。
!!! quote ""
仅支持 Linux且需要 `auto_route` 已启用。
仅支持 Linux且需要 `auto_route` 已启用。
自动配置 iptables/nftables 以重定向连接
通过使用 nftables 改善 TUN 路由和性能
*在 Android 中*
在 Linux 上始终推荐使用 `auto_redirect`,它提供更好的路由、更高的性能(优于 tproxy并避免了 TUN 和 Docker 桥接网络之间的冲突。
仅转发本地 IPv4 连接。 要通过热点或中继共享您的 VPN 连接,请使用 [VPNHotspot](https://github.com/Mygod/VPNHotspot)。
请注意,`auto_redirect` 也适用于 Android但由于缺少 `nftables``ip6tables`,仅执行简单的 IPv4 TCP 转发。
若要在 Android 上通过热点或中继器共享 VPN 连接,请使用 [VPNHotspot](https://github.com/Mygod/VPNHotspot)。
*在 Linux 中*:
`auto_redirect` 还会自动将兼容性规则插入 OpenWrt 的 fw4 表中,即无需额外配置即可在路由器上工作。
带有 `auto_redirect ``auto_route` 可以在路由器上按预期工作,**无需干预**
`route.default_mark``[dialOptions].routing_mark` 冲突
#### auto_redirect_input_mark
!!! question "自 sing-box 1.10.0 起"
`route_address_set``route_exclude_address_set` 使用的连接输入标记。
`auto_redirect` 使用的连接输入标记。
默认使用 `0x2023`
@@ -261,29 +266,25 @@ tun 接口的 IPv6 前缀。
!!! question "自 sing-box 1.10.0 起"
`route_address_set``route_exclude_address_set` 使用的连接输出标记。
`auto_redirect` 使用的连接输出标记。
默认使用 `0x2024`
#### strict_route
启用 `auto_route` 时执行严格的路由规则
启用 `auto_route`,强制执行严格的路由规则
*在 Linux 中*:
*在 Linux 中*
* 不支持的网络无法到达
* 使 ICMP 流量路由到 tun 而不是上游接口
* 将所有连接路由到 tun
* 使不支持的网络不可达。
* 出于历史遗留原因,当未启用 `strict_route``auto_redirect` 时,所有 ICMP 流量将不会通过 TUN。
它可以防止 IP 地址泄漏,并使 DNS 劫持在 Android 上工作。
*在 Windows 中*
*在 Windows 中*:
* 使不支持的网络不可达。
* 阻止 Windows 的 [普通多宿主 DNS 解析行为](https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2008-R2-and-2008/dd197552%28v%3Dws.10%29) 造成的 DNS 泄露
* 添加防火墙规则以阻止 Windows
的 [普通多宿主 DNS 解析行为](https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2008-R2-and-2008/dd197552%28v%3Dws.10%29)
造成的 DNS 泄露
它可能会使某些应用程序(如 VirtualBox在某些情况下无法正常工作。
它可能会使某些 Windows 应用程序(如 VirtualBox在某些情况下无法正常工作。
#### route_address
@@ -341,8 +342,6 @@ tun 接口的 IPv6 前缀。
将指定规则集中的目标 IP CIDR 规则添加到防火墙。
不匹配的流量将绕过 sing-box 路由。
`route.default_mark``[dialOptions].routing_mark` 冲突。
=== "`auto_redirect` 未启用"

View File

@@ -26,7 +26,7 @@ If enabled in the inbound, the protocol and domain name (if present) of by the c
| QUIC Client | Type |
|:------------------------:|:----------:|
| Chromium/Cronet | `chrimium` |
| Chromium/Cronet | `chromium` |
| Safari/Apple Network API | `safari` |
| Firefox / uquic firefox | `firefox` |
| quic-go / uquic chrome | `quic-go` |
| quic-go / uquic chrome | `quic-go` |

View File

@@ -1,186 +0,0 @@
package clashapi
import (
"archive/zip"
"context"
"crypto/x509"
"encoding/pem"
"io"
"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("/crt", getCertificate(ctx))
r.Get("/pem", getCertificatePEM(ctx))
r.Get("/magisk", getMagiskModule(ctx))
return r
}
func getMobileConfig(ctx context.Context) http.HandlerFunc {
return func(writer http.ResponseWriter, request *http.Request) {
store := service.FromContext[adapter.CertificateStore](ctx)
if !store.TLSDecryptionEnabled() {
http.NotFound(writer, request)
render.PlainText(writer, request, "TLS decryption not enabled")
return
}
certificate := store.TLSDecryptionCertificate()
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) {
store := service.FromContext[adapter.CertificateStore](ctx)
if !store.TLSDecryptionEnabled() {
http.NotFound(writer, request)
render.PlainText(writer, request, "TLS decryption not enabled")
return
}
writer.Header().Set("Content-Type", "application/x-x509-ca-cert")
writer.Header().Set("Content-Disposition", "attachment; filename=Certificate.crt")
writer.Write(store.TLSDecryptionCertificate().Raw)
}
}
func getCertificatePEM(ctx context.Context) http.HandlerFunc {
return func(writer http.ResponseWriter, request *http.Request) {
store := service.FromContext[adapter.CertificateStore](ctx)
if !store.TLSDecryptionEnabled() {
http.NotFound(writer, request)
render.PlainText(writer, request, "TLS decryption not enabled")
return
}
writer.Header().Set("Content-Type", "application/x-pem-file")
writer.Header().Set("Content-Disposition", "attachment; filename=Certificate.pem")
writer.Write(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: store.TLSDecryptionCertificate().Raw}))
}
}
func getMagiskModule(ctx context.Context) http.HandlerFunc {
return func(writer http.ResponseWriter, request *http.Request) {
store := service.FromContext[adapter.CertificateStore](ctx)
if !store.TLSDecryptionEnabled() {
http.NotFound(writer, request)
render.PlainText(writer, request, "TLS decryption not enabled")
return
}
writer.Header().Set("Content-Type", "application/zip")
writer.Header().Set("Content-Disposition", "attachment; filename="+store.TLSDecryptionCertificate().Subject.CommonName+".zip")
createMagiskModule(writer, store.TLSDecryptionCertificate())
}
}
func createMagiskModule(writer io.Writer, certificate *x509.Certificate) error {
zipWriter := zip.NewWriter(writer)
defer zipWriter.Close()
moduleProp, err := zipWriter.Create("module.prop")
if err != nil {
return err
}
_, err = moduleProp.Write([]byte(`
id=sing-box-certificate
name=` + certificate.Subject.CommonName + `
version=v0.0.1
versionCode=1
author=sing-box
description=This module adds ` + certificate.Subject.CommonName + ` to the system trust store.
`))
if err != nil {
return err
}
certificateFile, err := zipWriter.Create("system/etc/security/cacerts/" + certificate.Subject.CommonName + ".pem")
if err != nil {
return err
}
err = pem.Encode(certificateFile, &pem.Block{Type: "CERTIFICATE", Bytes: certificate.Raw})
if err != nil {
return err
}
updateBinary, err := zipWriter.Create("META-INF/com/google/android/update-binary")
if err != nil {
return err
}
_, err = updateBinary.Write([]byte(`
#!/sbin/sh
#################
# Initialization
#################
umask 022
# echo before loading util_functions
ui_print() { echo "$1"; }
require_new_magisk() {
ui_print "*******************************"
ui_print " Please install Magisk v20.4+! "
ui_print "*******************************"
exit 1
}
#########################
# Load util_functions.sh
#########################
OUTFD=$2
ZIPFILE=$3
mount /data 2>/dev/null
[ -f /data/adb/magisk/util_functions.sh ] || require_new_magisk
. /data/adb/magisk/util_functions.sh
[ $MAGISK_VER_CODE -lt 20400 ] && require_new_magisk
install_module
exit 0
`))
if err != nil {
return err
}
updaterScript, err := zipWriter.Create("META-INF/com/google/android/updater-script")
if err != nil {
return err
}
_, err = updaterScript.Write([]byte("#MAGISK"))
if err != nil {
return err
}
return nil
}

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

@@ -33,7 +33,7 @@ func BaseContext(platformInterface PlatformInterface) context.Context {
})
}
}
return box.Context(context.Background(), include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry(), dnsRegistry)
return box.Context(context.Background(), include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry(), dnsRegistry, include.ServiceRegistry())
}
func parseConfig(ctx context.Context, configContent string) (option.Options, error) {
@@ -116,6 +116,10 @@ func (s *platformInterfaceStub) FindProcessInfo(ctx context.Context, network str
return nil, os.ErrInvalid
}
func (s *platformInterfaceStub) SendNotification(notification *platform.Notification) error {
return nil
}
type interfaceMonitorStub struct{}
func (s *interfaceMonitorStub) Start() error {
@@ -145,8 +149,11 @@ func (s *interfaceMonitorStub) RegisterCallback(callback tun.DefaultInterfaceUpd
func (s *interfaceMonitorStub) UnregisterCallback(element *list.Element[tun.DefaultInterfaceUpdateCallback]) {
}
func (s *platformInterfaceStub) SendNotification(notification *platform.Notification) error {
return nil
func (s *interfaceMonitorStub) RegisterMyInterface(interfaceName string) {
}
func (s *interfaceMonitorStub) MyInterface() string {
return ""
}
func FormatConfig(configContent string) (*StringBox, error) {

View File

@@ -15,9 +15,10 @@ var (
type platformDefaultInterfaceMonitor struct {
*platformInterfaceWrapper
element *list.Element[tun.NetworkUpdateCallback]
callbacks list.List[tun.DefaultInterfaceUpdateCallback]
logger logger.Logger
logger logger.Logger
element *list.Element[tun.NetworkUpdateCallback]
callbacks list.List[tun.DefaultInterfaceUpdateCallback]
myInterface string
}
func (m *platformDefaultInterfaceMonitor) Start() error {
@@ -102,3 +103,15 @@ func (m *platformDefaultInterfaceMonitor) updateDefaultInterface(interfaceName s
callback(newInterface, 0)
}
}
func (m *platformDefaultInterfaceMonitor) RegisterMyInterface(interfaceName string) {
m.defaultInterfaceAccess.Lock()
defer m.defaultInterfaceAccess.Unlock()
m.myInterface = interfaceName
}
func (m *platformDefaultInterfaceMonitor) MyInterface() string {
m.defaultInterfaceAccess.Lock()
defer m.defaultInterfaceAccess.Unlock()
return m.myInterface
}

View File

@@ -39,7 +39,7 @@ type BoxService struct {
clashServer adapter.ClashServer
pauseManager pause.Manager
servicePauseFields
iOSPauseFields
}
func NewService(configContent string, platformInterface PlatformInterface) (*BoxService, error) {
@@ -164,6 +164,7 @@ func (w *platformInterfaceWrapper) OpenTun(options *tun.Options, platformOptions
if err != nil {
return nil, E.Cause(err, "query tun name")
}
options.InterfaceMonitor.RegisterMyInterface(options.Name)
dupFd, err := dup(int(tunFd))
if err != nil {
return nil, E.Cause(err, "dup tun file descriptor")

View File

@@ -1,31 +1,33 @@
package libbox
import (
"sync"
"time"
C "github.com/sagernet/sing-box/constant"
)
type servicePauseFields struct {
pauseAccess sync.Mutex
pauseTimer *time.Timer
type iOSPauseFields struct {
endPauseTimer *time.Timer
}
func (s *BoxService) Pause() {
s.pauseAccess.Lock()
defer s.pauseAccess.Unlock()
if s.pauseTimer != nil {
s.pauseTimer.Stop()
s.pauseManager.DevicePause()
if !C.IsIos {
s.instance.Router().ResetNetwork()
} else {
if s.endPauseTimer == nil {
s.endPauseTimer = time.AfterFunc(time.Minute, s.pauseManager.DeviceWake)
} else {
s.endPauseTimer.Reset(time.Minute)
}
}
s.pauseTimer = time.AfterFunc(3*time.Second, s.ResetNetwork)
}
func (s *BoxService) Wake() {
s.pauseAccess.Lock()
defer s.pauseAccess.Unlock()
if s.pauseTimer != nil {
s.pauseTimer.Stop()
if !C.IsIos {
s.pauseManager.DeviceWake()
s.instance.Router().ResetNetwork()
}
s.pauseTimer = time.AfterFunc(3*time.Minute, s.ResetNetwork)
}
func (s *BoxService) ResetNetwork() {

17
go.mod
View File

@@ -3,13 +3,15 @@ module github.com/sagernet/sing-box
go 1.23.1
require (
github.com/anytls/sing-anytls v0.0.6
github.com/anytls/sing-anytls v0.0.8
github.com/caddyserver/certmagic v0.21.7
github.com/cloudflare/circl v1.6.0
github.com/coder/websocket v1.8.12
github.com/cretz/bine v0.2.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.1
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466
github.com/gofrs/uuid/v5 v5.3.2
github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905
github.com/libdns/alidns v1.0.3
github.com/libdns/cloudflare v0.1.1
@@ -26,16 +28,16 @@ require (
github.com/sagernet/gvisor v0.0.0-20250325023245-7a9c0f5725fb
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.6-0.20250326051824-d39c2c2fddfa
github.com/sagernet/sing v0.6.7-0.20250409030945-77e2a1bb577c
github.com/sagernet/sing-mux v0.3.1
github.com/sagernet/sing-quic v0.4.1-beta.1
github.com/sagernet/sing-quic v0.4.1
github.com/sagernet/sing-shadowsocks v0.2.7
github.com/sagernet/sing-shadowsocks2 v0.2.0
github.com/sagernet/sing-shadowtls v0.2.1-0.20250316154757-6f9e732e5056
github.com/sagernet/sing-tun v0.6.2-0.20250319123703-35b5747b44ec
github.com/sagernet/sing-tun v0.6.5-0.20250412112220-15069fc1c20a
github.com/sagernet/sing-vmess v0.2.0
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7
github.com/sagernet/tailscale v1.80.3-mod.0
github.com/sagernet/tailscale v1.80.3-mod.2
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
@@ -53,7 +55,6 @@ require (
google.golang.org/grpc v1.70.0
google.golang.org/protobuf v1.36.5
howett.net/plist v1.0.1
software.sslmate.com/src/go-pkcs12 v0.4.0
)
//replace github.com/sagernet/sing => ../sing
@@ -67,7 +68,6 @@ require (
github.com/bits-and-blooms/bitset v1.13.0 // indirect
github.com/caddyserver/zerossl v0.1.3 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // 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
@@ -81,7 +81,6 @@ require (
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

24
go.sum
View File

@@ -8,8 +8,8 @@ github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7V
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/anytls/sing-anytls v0.0.6 h1:UatIjl/OvzWQGXQ1I2bAIkabL9WtihW0fA7G+DXGBUg=
github.com/anytls/sing-anytls v0.0.6/go.mod h1:7rjN6IukwysmdusYsrV51Fgu1uW6vsrdd6ctjnEAln8=
github.com/anytls/sing-anytls v0.0.8 h1:1u/fnH1HoeeMV5mX7/eUOjLBvPdkd1UJRmXiRi6Vymc=
github.com/anytls/sing-anytls v0.0.8/go.mod h1:7rjN6IukwysmdusYsrV51Fgu1uW6vsrdd6ctjnEAln8=
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.21.7 h1:66KJioPFJwttL43KYSWk7ErSmE6LfaJgCQuhm8Sg6fg=
@@ -66,8 +66,8 @@ 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.1 h1:aPx49MwJbekCzOyhZDjJVb0hx3A0KLjlbLx6p2gY0p0=
github.com/gofrs/uuid/v5 v5.3.1/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
github.com/gofrs/uuid/v5 v5.3.2 h1:2jfO8j3XgSwlz/wHqemAEugfnTlikAYHhnqQ8Xh4fE0=
github.com/gofrs/uuid/v5 v5.3.2/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
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/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
@@ -178,26 +178,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.6-0.20250326051824-d39c2c2fddfa h1:18mz8gmh0/EL3Bk+hB0Xf3tGOO1p/tP1sjjhSDeyUtU=
github.com/sagernet/sing v0.6.6-0.20250326051824-d39c2c2fddfa/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing v0.6.7-0.20250409030945-77e2a1bb577c h1:Zi+WR7f9SQ96yNHmyxj42BtaVb3kTouQ8bQLBHReTSI=
github.com/sagernet/sing v0.6.7-0.20250409030945-77e2a1bb577c/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.1-beta.1 h1:V2VfMckT3EQR3ZdfSzJgZZDsvfZZH42QAZpnOnHKa0s=
github.com/sagernet/sing-quic v0.4.1-beta.1/go.mod h1:c+CytOEyeN20KCTFIP8YQUkNDVFLSzjrEPqP7Hlnxys=
github.com/sagernet/sing-quic v0.4.1 h1:pxlMa4efZu/M07RgGagNNDDyl6ZUwpmNUjRTpgHOWK4=
github.com/sagernet/sing-quic v0.4.1/go.mod h1:tqPa0/Wqa19MkkSlKVZZX5sHxtiDR9BROcn4ufcbVdY=
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.1-0.20250316154757-6f9e732e5056 h1:GFNJQAHhSXqAfxAw1wDG/QWbdpGH5Na3k8qUynqWnEA=
github.com/sagernet/sing-shadowtls v0.2.1-0.20250316154757-6f9e732e5056/go.mod h1:HyacBPIFiKihJQR8LQp56FM4hBtd/7MZXnRxxQIOPsc=
github.com/sagernet/sing-tun v0.6.2-0.20250319123703-35b5747b44ec h1:9/OYGb9qDmUFIhqd3S+3eni62EKRQR1rSmRH18baA/M=
github.com/sagernet/sing-tun v0.6.2-0.20250319123703-35b5747b44ec/go.mod h1:fisFCbC4Vfb6HqQNcwPJi2CDK2bf0Xapyz3j3t4cnHE=
github.com/sagernet/sing-tun v0.6.5-0.20250412112220-15069fc1c20a h1:2aLxZFD2HPCLrnFGpH+KBuPqMOk0cuaDE2dgEvANuMk=
github.com/sagernet/sing-tun v0.6.5-0.20250412112220-15069fc1c20a/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.80.3-mod.0 h1:oHIdivbR/yxoiA9d3a2rRlhYn2shY9XVF35Rr8jW508=
github.com/sagernet/tailscale v1.80.3-mod.0/go.mod h1:EBxXsWu4OH2ELbQLq32WoBeIubG8KgDrg4/Oaxjs6lI=
github.com/sagernet/tailscale v1.80.3-mod.2 h1:hT0CI74q727EuCcgQ+T4pvon8V0aoi4vTAxah7GsNMQ=
github.com/sagernet/tailscale v1.80.3-mod.2/go.mod h1:EBxXsWu4OH2ELbQLq32WoBeIubG8KgDrg4/Oaxjs6lI=
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=

View File

@@ -7,6 +7,7 @@ import (
"github.com/sagernet/sing-box/adapter/endpoint"
"github.com/sagernet/sing-box/adapter/inbound"
"github.com/sagernet/sing-box/adapter/outbound"
"github.com/sagernet/sing-box/adapter/service"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/dns/transport"
@@ -33,6 +34,8 @@ import (
"github.com/sagernet/sing-box/protocol/tun"
"github.com/sagernet/sing-box/protocol/vless"
"github.com/sagernet/sing-box/protocol/vmess"
"github.com/sagernet/sing-box/service/resolved"
"github.com/sagernet/sing-box/service/ssmapi"
E "github.com/sagernet/sing/common/exceptions"
)
@@ -110,6 +113,7 @@ func DNSTransportRegistry() *dns.TransportRegistry {
hosts.RegisterTransport(registry)
local.RegisterTransport(registry)
fakeip.RegisterTransport(registry)
resolved.RegisterTransport(registry)
registerQUICTransports(registry)
registerDHCPTransport(registry)
@@ -118,6 +122,17 @@ func DNSTransportRegistry() *dns.TransportRegistry {
return registry
}
func ServiceRegistry() *service.Registry {
registry := service.NewRegistry()
resolved.RegisterService(registry)
ssmapi.RegisterService(registry)
registerDERPService(registry)
return registry
}
func registerStubForRemovedInbounds(registry *inbound.Registry) {
inbound.Register[option.ShadowsocksInboundOptions](registry, C.TypeShadowsocksR, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksInboundOptions) (adapter.Inbound, error) {
return nil, E.New("ShadowsocksR is deprecated and removed in sing-box 1.6.0")

View File

@@ -4,8 +4,10 @@ package include
import (
"github.com/sagernet/sing-box/adapter/endpoint"
"github.com/sagernet/sing-box/adapter/service"
"github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/protocol/tailscale"
"github.com/sagernet/sing-box/service/derp"
)
func registerTailscaleEndpoint(registry *endpoint.Registry) {
@@ -15,3 +17,8 @@ func registerTailscaleEndpoint(registry *endpoint.Registry) {
func registerTailscaleTransport(registry *dns.TransportRegistry) {
tailscale.RegistryTransport(registry)
}
func registerDERPService(registry *service.Registry) {
derp.Register(registry)
derp.RegisterSTUN(registry)
}

View File

@@ -7,6 +7,7 @@ import (
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/adapter/endpoint"
"github.com/sagernet/sing-box/adapter/service"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/log"
@@ -25,3 +26,12 @@ func registerTailscaleTransport(registry *dns.TransportRegistry) {
return nil, E.New(`Tailscale is not included in this build, rebuild with -tags with_tailscale`)
})
}
func registerDERPService(registry *service.Registry) {
service.Register[option.DERPServiceOptions](registry, C.TypeDERP, func(ctx context.Context, logger log.ContextLogger, tag string, options option.DERPServiceOptions) (adapter.Service, error) {
return nil, E.New(`DERP is not included in this build, rebuild with -tags with_tailscale`)
})
service.Register[option.DERPSTUNServiceOptions](registry, C.TypeDERP, func(ctx context.Context, logger log.ContextLogger, tag string, options option.DERPSTUNServiceOptions) (adapter.Service, error) {
return nil, E.New(`STUN (DERP) 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="))
})

View File

@@ -1,811 +0,0 @@
package mitm
import (
"bufio"
"bytes"
"context"
"crypto/tls"
"io"
"math"
"mime"
"net"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"unicode"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/dialer"
sTLS "github.com/sagernet/sing-box/common/tls"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/atomic"
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/common/ntp"
sHTTP "github.com/sagernet/sing/protocol/http"
"github.com/sagernet/sing/service"
"golang.org/x/net/http2"
)
var _ adapter.MITMEngine = (*Engine)(nil)
type Engine struct {
ctx context.Context
logger logger.ContextLogger
connection adapter.ConnectionManager
certificate adapter.CertificateStore
timeFunc func() time.Time
http2Enabled bool
}
func NewEngine(ctx context.Context, logger logger.ContextLogger, options option.MITMOptions) (*Engine, error) {
engine := &Engine{
ctx: ctx,
logger: logger,
http2Enabled: options.HTTP2Enabled,
}
return engine, nil
}
func (e *Engine) Start(stage adapter.StartStage) error {
switch stage {
case adapter.StartStateInitialize:
e.connection = service.FromContext[adapter.ConnectionManager](e.ctx)
e.certificate = service.FromContext[adapter.CertificateStore](e.ctx)
e.timeFunc = ntp.TimeFuncFromContext(e.ctx)
if e.timeFunc == nil {
e.timeFunc = time.Now
}
}
return nil
}
func (e *Engine) Close() error {
return nil
}
func (e *Engine) NewConnection(ctx context.Context, this N.Dialer, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
if e.certificate.TLSDecryptionEnabled() && metadata.ClientHello != nil {
err := e.newTLS(ctx, this, conn, metadata, onClose)
if err != nil {
e.logger.ErrorContext(ctx, err)
} else {
e.logger.DebugContext(ctx, "connection closed")
}
if onClose != nil {
onClose(err)
}
return
} else if metadata.HTTPRequest != nil {
err := e.newHTTP1(ctx, this, conn, nil, metadata)
if err != nil {
e.logger.ErrorContext(ctx, err)
} else {
e.logger.DebugContext(ctx, "connection closed")
}
if onClose != nil {
onClose(err)
}
return
} else {
e.logger.DebugContext(ctx, "HTTP and TLS not detected, skipped")
}
metadata.MITM = nil
e.connection.NewConnection(ctx, this, conn, metadata, onClose)
}
func (e *Engine) newTLS(ctx context.Context, this N.Dialer, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) error {
acceptHTTP := len(metadata.ClientHello.SupportedProtos) == 0 || common.Contains(metadata.ClientHello.SupportedProtos, "http/1.1")
acceptH2 := e.http2Enabled && common.Contains(metadata.ClientHello.SupportedProtos, "h2")
if !acceptHTTP && !acceptH2 {
metadata.MITM = nil
e.logger.DebugContext(ctx, "unsupported application protocol: ", strings.Join(metadata.ClientHello.SupportedProtos, ","))
e.connection.NewConnection(ctx, this, conn, metadata, onClose)
return nil
}
var nextProtos []string
if acceptH2 {
nextProtos = append(nextProtos, "h2")
} else if acceptHTTP {
nextProtos = append(nextProtos, "http/1.1")
}
var (
maxVersion uint16
minVersion uint16
)
for _, version := range metadata.ClientHello.SupportedVersions {
maxVersion = common.Max(maxVersion, version)
minVersion = common.Min(minVersion, version)
}
serverName := metadata.ClientHello.ServerName
if serverName == "" && metadata.Destination.IsIP() {
serverName = metadata.Destination.Addr.String()
}
tlsConfig := &tls.Config{
Time: e.timeFunc,
ServerName: serverName,
NextProtos: nextProtos,
MinVersion: minVersion,
MaxVersion: maxVersion,
GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
return sTLS.GenerateKeyPair(e.certificate.TLSDecryptionCertificate(), e.certificate.TLSDecryptionPrivateKey(), e.timeFunc, serverName)
},
}
tlsConn := tls.Server(conn, tlsConfig)
err := tlsConn.HandshakeContext(ctx)
if err != nil {
return E.Cause(err, "TLS handshake failed for ", metadata.ClientHello.ServerName, ", ", strings.Join(metadata.ClientHello.SupportedProtos, ", "))
}
if tlsConn.ConnectionState().NegotiatedProtocol == "h2" {
return e.newHTTP2(ctx, this, tlsConn, tlsConfig, metadata, onClose)
} else {
return e.newHTTP1(ctx, this, tlsConn, tlsConfig, metadata)
}
}
func (e *Engine) newHTTP1(ctx context.Context, this N.Dialer, conn net.Conn, tlsConfig *tls.Config, metadata adapter.InboundContext) error {
options := metadata.MITM
defer conn.Close()
reader := bufio.NewReader(conn)
request, err := sHTTP.ReadRequest(reader)
if err != nil {
return E.Cause(err, "read HTTP request")
}
rawRequestURL := request.URL
if tlsConfig != nil {
rawRequestURL.Scheme = "https"
} else {
rawRequestURL.Scheme = "http"
}
if rawRequestURL.Host == "" {
rawRequestURL.Host = request.Host
}
requestURL := rawRequestURL.String()
request.RequestURI = ""
var requestMatch bool
var body []byte
if options.Print && request.ContentLength > 0 && request.ContentLength <= 131072 {
body, err = io.ReadAll(request.Body)
if err != nil {
return E.Cause(err, "read HTTP request body")
}
request.Body = io.NopCloser(bytes.NewReader(body))
}
if options.Print {
e.printRequest(ctx, request, body)
}
for i, rule := range options.SurgeURLRewrite {
if !rule.Pattern.MatchString(requestURL) {
continue
}
e.logger.DebugContext(ctx, "match url_rewrite[", i, "] => ", rule.String())
if rule.Reject {
return E.New("request rejected by url_rewrite")
} else if rule.Redirect {
w := new(simpleResponseWriter)
http.Redirect(w, request, rule.Destination.String(), http.StatusFound)
err = w.Build(request).Write(conn)
if err != nil {
return E.Cause(err, "write url_rewrite 302 response")
}
return nil
}
requestMatch = true
request.URL = rule.Destination
newDestination := M.ParseSocksaddrHostPortStr(rule.Destination.Hostname(), rule.Destination.Port())
if newDestination.Port == 0 {
newDestination.Port = metadata.Destination.Port
}
metadata.Destination = newDestination
if tlsConfig != nil {
tlsConfig.ServerName = rule.Destination.Hostname()
}
break
}
for i, rule := range options.SurgeHeaderRewrite {
if rule.Response {
continue
}
if !rule.Pattern.MatchString(requestURL) {
continue
}
requestMatch = true
e.logger.DebugContext(ctx, "match header_rewrite[", i, "] => ", rule.String())
switch {
case rule.Add:
if strings.ToLower(rule.Key) == "host" {
request.Host = rule.Value
continue
}
request.Header.Add(rule.Key, rule.Value)
case rule.Delete:
request.Header.Del(rule.Key)
case rule.Replace:
if request.Header.Get(rule.Key) != "" {
request.Header.Set(rule.Key, rule.Value)
}
case rule.ReplaceRegex:
if value := request.Header.Get(rule.Key); value != "" {
request.Header.Set(rule.Key, rule.Match.ReplaceAllString(value, rule.Value))
}
}
}
for i, rule := range options.SurgeBodyRewrite {
if rule.Response {
continue
}
if !rule.Pattern.MatchString(requestURL) {
continue
}
requestMatch = true
e.logger.DebugContext(ctx, "match body_rewrite[", i, "] => ", rule.String())
if body == nil {
if request.ContentLength <= 0 {
e.logger.WarnContext(ctx, "body replace skipped due to non-fixed content length")
break
} else if request.ContentLength > 131072 {
e.logger.WarnContext(ctx, "body replace skipped due to large content length: ", request.ContentLength)
break
}
body, err = io.ReadAll(request.Body)
if err != nil {
return E.Cause(err, "read HTTP request body")
}
}
for mi := 0; i < len(rule.Match); i++ {
body = rule.Match[mi].ReplaceAll(body, []byte(rule.Replace[i]))
}
request.Body = io.NopCloser(bytes.NewReader(body))
request.ContentLength = int64(len(body))
}
if !requestMatch {
for i, rule := range options.SurgeMapLocal {
if !rule.Pattern.MatchString(requestURL) {
continue
}
requestMatch = true
e.logger.DebugContext(ctx, "match map_local[", i, "] => ", rule.String())
var (
statusCode = http.StatusOK
headers = make(http.Header)
)
if rule.StatusCode > 0 {
statusCode = rule.StatusCode
}
switch {
case rule.File:
resource, err := os.ReadFile(rule.Data)
if err != nil {
return E.Cause(err, "open map local source")
}
mimeType := mime.TypeByExtension(filepath.Ext(rule.Data))
if mimeType == "" {
mimeType = "application/octet-stream"
}
headers.Set("Content-Type", mimeType)
body = resource
case rule.Text:
headers.Set("Content-Type", "text/plain")
body = []byte(rule.Data)
case rule.TinyGif:
headers.Set("Content-Type", "image/gif")
body = surgeTinyGif()
case rule.Base64:
headers.Set("Content-Type", "application/octet-stream")
body = rule.Base64Data
}
response := &http.Response{
StatusCode: statusCode,
Status: http.StatusText(statusCode),
Proto: request.Proto,
ProtoMajor: request.ProtoMajor,
ProtoMinor: request.ProtoMinor,
Header: headers,
Body: io.NopCloser(bytes.NewReader(body)),
}
err = response.Write(conn)
if err != nil {
return E.Cause(err, "write map local response")
}
return nil
}
}
ctx = adapter.WithContext(ctx, &metadata)
var innerErr atomic.TypedValue[error]
httpClient := &http.Client{
Transport: &http.Transport{
DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
if len(metadata.DestinationAddresses) > 0 || metadata.Destination.IsIP() {
return dialer.DialSerialNetwork(ctx, this, N.NetworkTCP, metadata.Destination, metadata.DestinationAddresses, metadata.NetworkStrategy, metadata.NetworkType, metadata.FallbackNetworkType, metadata.FallbackDelay)
} else {
return this.DialContext(ctx, N.NetworkTCP, metadata.Destination)
}
},
TLSClientConfig: tlsConfig,
},
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
defer httpClient.CloseIdleConnections()
requestCtx, cancel := context.WithCancel(ctx)
defer cancel()
response, err := httpClient.Do(request.WithContext(requestCtx))
if err != nil {
cancel()
return E.Errors(innerErr.Load(), err)
}
var responseMatch bool
var responseBody []byte
if options.Print && response.ContentLength > 0 && response.ContentLength <= 131072 {
responseBody, err = io.ReadAll(response.Body)
if err != nil {
return E.Cause(err, "read HTTP response body")
}
response.Body = io.NopCloser(bytes.NewReader(responseBody))
}
if options.Print {
e.printResponse(ctx, request, response, responseBody)
}
for i, rule := range options.SurgeHeaderRewrite {
if !rule.Response {
continue
}
if !rule.Pattern.MatchString(requestURL) {
continue
}
responseMatch = true
e.logger.DebugContext(ctx, "match header_rewrite[", i, "] => ", rule.String())
switch {
case rule.Add:
response.Header.Add(rule.Key, rule.Value)
case rule.Delete:
response.Header.Del(rule.Key)
case rule.Replace:
if response.Header.Get(rule.Key) != "" {
response.Header.Set(rule.Key, rule.Value)
}
case rule.ReplaceRegex:
if value := response.Header.Get(rule.Key); value != "" {
response.Header.Set(rule.Key, rule.Match.ReplaceAllString(value, rule.Value))
}
}
}
for i, rule := range options.SurgeBodyRewrite {
if !rule.Response {
continue
}
if !rule.Pattern.MatchString(requestURL) {
continue
}
responseMatch = true
e.logger.DebugContext(ctx, "match body_rewrite[", i, "] => ", rule.String())
if responseBody == nil {
if response.ContentLength <= 0 {
e.logger.WarnContext(ctx, "body replace skipped due to non-fixed content length")
break
} else if response.ContentLength > 131072 {
e.logger.WarnContext(ctx, "body replace skipped due to large content length: ", request.ContentLength)
break
}
responseBody, err = io.ReadAll(response.Body)
if err != nil {
return E.Cause(err, "read HTTP request body")
}
}
for mi := 0; i < len(rule.Match); i++ {
responseBody = rule.Match[mi].ReplaceAll(responseBody, []byte(rule.Replace[i]))
}
response.Body = io.NopCloser(bytes.NewReader(responseBody))
response.ContentLength = int64(len(responseBody))
}
if !options.Print && !requestMatch && !responseMatch {
e.logger.WarnContext(ctx, "request not modified")
}
err = response.Write(conn)
if err != nil {
return E.Errors(E.Cause(err, "write HTTP response"), innerErr.Load())
} else if innerErr.Load() != nil {
return E.Cause(innerErr.Load(), "write HTTP response")
}
return nil
}
func (e *Engine) newHTTP2(ctx context.Context, this N.Dialer, conn net.Conn, tlsConfig *tls.Config, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) error {
httpTransport := &http.Transport{
ForceAttemptHTTP2: true,
DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
ctx = adapter.WithContext(ctx, &metadata)
if len(metadata.DestinationAddresses) > 0 || metadata.Destination.IsIP() {
return dialer.DialSerialNetwork(ctx, this, N.NetworkTCP, metadata.Destination, metadata.DestinationAddresses, metadata.NetworkStrategy, metadata.NetworkType, metadata.FallbackNetworkType, metadata.FallbackDelay)
} else {
return this.DialContext(ctx, N.NetworkTCP, metadata.Destination)
}
},
TLSClientConfig: tlsConfig,
}
err := http2.ConfigureTransport(httpTransport)
if err != nil {
return E.Cause(err, "configure HTTP/2 transport")
}
handler := &engineHandler{
Engine: e,
conn: conn,
tlsConfig: tlsConfig,
dialer: this,
metadata: metadata,
httpClient: &http.Client{
Transport: httpTransport,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
},
onClose: onClose,
}
http2Server := &http2.Server{
MaxReadFrameSize: math.MaxUint32,
}
http2Server.ServeConn(conn, &http2.ServeConnOpts{
Context: ctx,
Handler: handler,
})
return nil
}
type engineHandler struct {
*Engine
conn net.Conn
tlsConfig *tls.Config
dialer N.Dialer
metadata adapter.InboundContext
onClose N.CloseHandlerFunc
httpClient *http.Client
}
func (e *engineHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
err := e.serveHTTP(request.Context(), writer, request)
if err != nil {
if E.IsClosedOrCanceled(err) {
e.logger.DebugContext(request.Context(), E.Cause(err, "connection closed"))
} else {
e.logger.ErrorContext(request.Context(), err)
}
}
}
func (e *engineHandler) serveHTTP(ctx context.Context, writer http.ResponseWriter, request *http.Request) error {
options := e.metadata.MITM
rawRequestURL := request.URL
rawRequestURL.Scheme = "https"
if rawRequestURL.Host == "" {
rawRequestURL.Host = request.Host
}
requestURL := rawRequestURL.String()
request.RequestURI = ""
var requestMatch bool
var (
body []byte
err error
)
if options.Print && request.ContentLength > 0 && request.ContentLength <= 131072 {
body, err = io.ReadAll(request.Body)
if err != nil {
return E.Cause(err, "read HTTP request body")
}
request.Body.Close()
request.Body = io.NopCloser(bytes.NewReader(body))
}
if options.Print {
e.printRequest(ctx, request, body)
}
for i, rule := range options.SurgeURLRewrite {
if !rule.Pattern.MatchString(requestURL) {
continue
}
e.logger.DebugContext(ctx, "match url_rewrite[", i, "] => ", rule.String())
if rule.Reject {
return E.New("request rejected by url_rewrite")
} else if rule.Redirect {
http.Redirect(writer, request, rule.Destination.String(), http.StatusFound)
return nil
}
requestMatch = true
request.URL = rule.Destination
newDestination := M.ParseSocksaddrHostPortStr(rule.Destination.Hostname(), rule.Destination.Port())
if newDestination.Port == 0 {
newDestination.Port = e.metadata.Destination.Port
}
e.metadata.Destination = newDestination
e.tlsConfig.ServerName = rule.Destination.Hostname()
break
}
for i, rule := range options.SurgeHeaderRewrite {
if rule.Response {
continue
}
if !rule.Pattern.MatchString(requestURL) {
continue
}
requestMatch = true
e.logger.DebugContext(ctx, "match header_rewrite[", i, "] => ", rule.String())
switch {
case rule.Add:
if strings.ToLower(rule.Key) == "host" {
request.Host = rule.Value
continue
}
request.Header.Add(rule.Key, rule.Value)
case rule.Delete:
request.Header.Del(rule.Key)
case rule.Replace:
if request.Header.Get(rule.Key) != "" {
request.Header.Set(rule.Key, rule.Value)
}
case rule.ReplaceRegex:
if value := request.Header.Get(rule.Key); value != "" {
request.Header.Set(rule.Key, rule.Match.ReplaceAllString(value, rule.Value))
}
}
}
for i, rule := range options.SurgeBodyRewrite {
if rule.Response {
continue
}
if !rule.Pattern.MatchString(requestURL) {
continue
}
requestMatch = true
e.logger.DebugContext(ctx, "match body_rewrite[", i, "] => ", rule.String())
var body []byte
if request.ContentLength <= 0 {
e.logger.WarnContext(ctx, "body replace skipped due to non-fixed content length")
break
} else if request.ContentLength > 131072 {
e.logger.WarnContext(ctx, "body replace skipped due to large content length: ", request.ContentLength)
break
}
body, err := io.ReadAll(request.Body)
if err != nil {
return E.Cause(err, "read HTTP request body")
}
request.Body.Close()
for mi := 0; i < len(rule.Match); i++ {
body = rule.Match[mi].ReplaceAll(body, []byte(rule.Replace[i]))
}
request.Body = io.NopCloser(bytes.NewReader(body))
request.ContentLength = int64(len(body))
}
if !requestMatch {
for i, rule := range options.SurgeMapLocal {
if !rule.Pattern.MatchString(requestURL) {
continue
}
requestMatch = true
e.logger.DebugContext(ctx, "match map_local[", i, "] => ", rule.String())
go func() {
io.Copy(io.Discard, request.Body)
request.Body.Close()
}()
var (
statusCode = http.StatusOK
headers = make(http.Header)
body []byte
)
if rule.StatusCode > 0 {
statusCode = rule.StatusCode
}
switch {
case rule.File:
resource, err := os.ReadFile(rule.Data)
if err != nil {
return E.Cause(err, "open map local source")
}
mimeType := mime.TypeByExtension(filepath.Ext(rule.Data))
if mimeType == "" {
mimeType = "application/octet-stream"
}
headers.Set("Content-Type", mimeType)
body = resource
case rule.Text:
headers.Set("Content-Type", "text/plain")
body = []byte(rule.Data)
case rule.TinyGif:
headers.Set("Content-Type", "image/gif")
body = surgeTinyGif()
case rule.Base64:
headers.Set("Content-Type", "application/octet-stream")
body = rule.Base64Data
}
for key, values := range headers {
writer.Header()[key] = values
}
writer.WriteHeader(statusCode)
_, err = writer.Write(body)
if err != nil {
return E.Cause(err, "write map local response")
}
return nil
}
}
requestCtx, cancel := context.WithCancel(ctx)
defer cancel()
response, err := e.httpClient.Do(request.WithContext(requestCtx))
if err != nil {
cancel()
return E.Cause(err, "exchange request")
}
var responseMatch bool
var responseBody []byte
if options.Print && response.ContentLength > 0 && response.ContentLength <= 131072 {
responseBody, err = io.ReadAll(response.Body)
if err != nil {
return E.Cause(err, "read HTTP response body")
}
response.Body.Close()
response.Body = io.NopCloser(bytes.NewReader(responseBody))
}
if options.Print {
e.printResponse(ctx, request, response, responseBody)
}
for i, rule := range options.SurgeHeaderRewrite {
if !rule.Response {
continue
}
if !rule.Pattern.MatchString(requestURL) {
continue
}
responseMatch = true
e.logger.DebugContext(ctx, "match header_rewrite[", i, "] => ", rule.String())
switch {
case rule.Add:
response.Header.Add(rule.Key, rule.Value)
case rule.Delete:
response.Header.Del(rule.Key)
case rule.Replace:
if response.Header.Get(rule.Key) != "" {
response.Header.Set(rule.Key, rule.Value)
}
case rule.ReplaceRegex:
if value := response.Header.Get(rule.Key); value != "" {
response.Header.Set(rule.Key, rule.Match.ReplaceAllString(value, rule.Value))
}
}
}
for i, rule := range options.SurgeBodyRewrite {
if !rule.Response {
continue
}
if !rule.Pattern.MatchString(requestURL) {
continue
}
responseMatch = true
e.logger.DebugContext(ctx, "match body_rewrite[", i, "] => ", rule.String())
if responseBody == nil {
if response.ContentLength <= 0 {
e.logger.WarnContext(ctx, "body replace skipped due to non-fixed content length")
break
} else if response.ContentLength > 131072 {
e.logger.WarnContext(ctx, "body replace skipped due to large content length: ", request.ContentLength)
break
}
responseBody, err = io.ReadAll(response.Body)
if err != nil {
return E.Cause(err, "read HTTP request body")
}
response.Body.Close()
}
for mi := 0; i < len(rule.Match); i++ {
responseBody = rule.Match[mi].ReplaceAll(responseBody, []byte(rule.Replace[i]))
}
response.Body = io.NopCloser(bytes.NewReader(responseBody))
response.ContentLength = int64(len(responseBody))
}
if !options.Print && !requestMatch && !responseMatch {
e.logger.WarnContext(ctx, "request not modified")
}
for key, values := range response.Header {
writer.Header()[key] = values
}
writer.WriteHeader(response.StatusCode)
_, err = io.Copy(writer, response.Body)
response.Body.Close()
if err != nil {
return E.Cause(err, "write HTTP response")
}
return nil
}
func (e *Engine) printRequest(ctx context.Context, request *http.Request, body []byte) {
var builder strings.Builder
builder.WriteString(F.ToString(request.Proto, " ", request.Method, " ", request.URL))
builder.WriteString("\n")
if request.URL.Hostname() != "" && request.URL.Hostname() != request.Host {
builder.WriteString("Host: ")
builder.WriteString(request.Host)
builder.WriteString("\n")
}
for key, values := range request.Header {
for _, value := range values {
builder.WriteString(key)
builder.WriteString(": ")
builder.WriteString(value)
builder.WriteString("\n")
}
}
if len(body) > 0 {
builder.WriteString("\n")
if !bytes.ContainsFunc(body, func(r rune) bool {
return !unicode.IsPrint(r) && !unicode.IsSpace(r)
}) {
builder.Write(body)
} else {
builder.WriteString("(body not printable)")
}
}
e.logger.InfoContext(ctx, "request: ", builder.String())
}
func (e *Engine) printResponse(ctx context.Context, request *http.Request, response *http.Response, body []byte) {
var builder strings.Builder
builder.WriteString(F.ToString(response.Proto, " ", response.Status, " ", request.URL))
builder.WriteString("\n")
for key, values := range response.Header {
for _, value := range values {
builder.WriteString(key)
builder.WriteString(": ")
builder.WriteString(value)
builder.WriteString("\n")
}
}
if len(body) > 0 {
builder.WriteString("\n")
if !bytes.ContainsFunc(body, func(r rune) bool {
return !unicode.IsPrint(r) && !unicode.IsSpace(r)
}) {
builder.Write(body)
} else {
builder.WriteString("(body not printable)")
}
}
e.logger.InfoContext(ctx, "response: ", builder.String())
}
type simpleResponseWriter struct {
statusCode int
header http.Header
body bytes.Buffer
}
func (w *simpleResponseWriter) Build(request *http.Request) *http.Response {
return &http.Response{
StatusCode: w.statusCode,
Status: http.StatusText(w.statusCode),
Proto: request.Proto,
ProtoMajor: request.ProtoMajor,
ProtoMinor: request.ProtoMinor,
Header: w.header,
Body: io.NopCloser(&w.body),
}
}
func (w *simpleResponseWriter) Header() http.Header {
if w.header == nil {
w.header = make(http.Header)
}
return w.header
}
func (w *simpleResponseWriter) Write(b []byte) (int, error) {
return w.body.Write(b)
}
func (w *simpleResponseWriter) WriteHeader(statusCode int) {
w.statusCode = statusCode
}

View File

@@ -11,13 +11,6 @@ type _CertificateOptions struct {
Certificate badoption.Listable[string] `json:"certificate,omitempty"`
CertificatePath badoption.Listable[string] `json:"certificate_path,omitempty"`
CertificateDirectoryPath badoption.Listable[string] `json:"certificate_directory_path,omitempty"`
TLSDecryption *TLSDecryptionOptions `json:"tls_decryption,omitempty"`
}
type TLSDecryptionOptions struct {
Enabled bool `json:"enabled,omitempty"`
KeyPair string `json:"key_pair_p12,omitempty"`
KeyPairPassword string `json:"key_pair_p12_password,omitempty"`
}
type CertificateOptions _CertificateOptions

View File

@@ -121,7 +121,6 @@ type LegacyDNSFakeIPOptions struct {
type DNSTransportOptionsRegistry interface {
CreateOptions(transportType string) (any, bool)
}
type _DNSServerOptions struct {
Type string `json:"type,omitempty"`
Tag string `json:"tag,omitempty"`

View File

@@ -32,11 +32,11 @@ func (h *Endpoint) UnmarshalJSONContext(ctx context.Context, content []byte) err
}
registry := service.FromContext[EndpointOptionsRegistry](ctx)
if registry == nil {
return E.New("missing Endpoint fields registry in context")
return E.New("missing endpoint fields registry in context")
}
options, loaded := registry.CreateOptions(h.Type)
if !loaded {
return E.New("unknown inbound type: ", h.Type)
return E.New("unknown endpoint type: ", h.Type)
}
err = badjson.UnmarshallExcludedContext(ctx, content, (*_Endpoint)(h), options)
if err != nil {

View File

@@ -34,7 +34,7 @@ func (h *Inbound) UnmarshalJSONContext(ctx context.Context, content []byte) erro
}
registry := service.FromContext[InboundOptionsRegistry](ctx)
if registry == nil {
return E.New("missing Inbound fields registry in context")
return E.New("missing inbound fields registry in context")
}
options, loaded := registry.CreateOptions(h.Type)
if !loaded {

View File

@@ -1,19 +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"`
}
type MITMRouteOptions struct {
Enabled bool `json:"enabled,omitempty"`
Print bool `json:"print,omitempty"`
SurgeURLRewrite badoption.Listable[SurgeURLRewriteLine] `json:"surge_url_rewrite,omitempty"`
SurgeHeaderRewrite badoption.Listable[SurgeHeaderRewriteLine] `json:"surge_header_rewrite,omitempty"`
SurgeBodyRewrite badoption.Listable[SurgeBodyRewriteLine] `json:"surge_body_rewrite,omitempty"`
SurgeMapLocal badoption.Listable[SurgeMapLocalLine] `json:"surge_map_local,omitempty"`
}

View File

@@ -1,449 +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)})
}
if l.StatusCode != 0 {
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
default:
return E.New("unsupported data-type ", dataTypeField.Value)
}
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 "header":
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: ", fields[i].Key)
}
}
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
}
}
continue
}
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: value, 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

@@ -4,6 +4,7 @@ import (
"bytes"
"context"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
)
@@ -12,14 +13,14 @@ 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"`
Services []Service `json:"services,omitempty"`
Experimental *ExperimentalOptions `json:"experimental,omitempty"`
NTP *NTPOptions `json:"ntp,omitempty"`
Certificate *CertificateOptions `json:"certificate,omitempty"`
MITM *MITMOptions `json:"mitm,omitempty"`
}
type Options _Options
@@ -32,7 +33,7 @@ func (o *Options) UnmarshalJSONContext(ctx context.Context, content []byte) erro
return err
}
o.RawMessage = content
return nil
return checkOptions(o)
}
type LogOptions struct {
@@ -44,3 +45,52 @@ type LogOptions struct {
}
type StubOptions struct{}
func checkOptions(options *Options) error {
err := checkInbounds(options.Inbounds)
if err != nil {
return err
}
err = checkOutbounds(options.Outbounds, options.Endpoints)
if err != nil {
return err
}
return nil
}
func checkInbounds(inbounds []Inbound) error {
seen := make(map[string]bool)
for _, inbound := range inbounds {
if inbound.Tag == "" {
continue
}
if seen[inbound.Tag] {
return E.New("duplicate inbound tag: ", inbound.Tag)
}
seen[inbound.Tag] = true
}
return nil
}
func checkOutbounds(outbounds []Outbound, endpoints []Endpoint) error {
seen := make(map[string]bool)
for _, outbound := range outbounds {
if outbound.Tag == "" {
continue
}
if seen[outbound.Tag] {
return E.New("duplicate outbound/endpoint tag: ", outbound.Tag)
}
seen[outbound.Tag] = true
}
for _, endpoint := range endpoints {
if endpoint.Tag == "" {
continue
}
if seen[endpoint.Tag] {
return E.New("duplicate outbound/endpoint tag: ", endpoint.Tag)
}
seen[endpoint.Tag] = true
}
return nil
}

49
option/resolved.go Normal file
View File

@@ -0,0 +1,49 @@
package option
import (
"context"
"net/netip"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badoption"
)
type _ResolvedServiceOptions struct {
ListenOptions
}
type ResolvedServiceOptions _ResolvedServiceOptions
func (r ResolvedServiceOptions) MarshalJSONContext(ctx context.Context) ([]byte, error) {
if r.Listen != nil && netip.Addr(*r.Listen) == (netip.AddrFrom4([4]byte{127, 0, 0, 53})) {
r.Listen = nil
}
if r.ListenPort == 53 {
r.ListenPort = 0
}
return json.MarshalContext(ctx, (*_ResolvedServiceOptions)(&r))
}
func (r *ResolvedServiceOptions) UnmarshalJSONContext(ctx context.Context, bytes []byte) error {
err := json.UnmarshalContextDisallowUnknownFields(ctx, bytes, (*_ResolvedServiceOptions)(r))
if err != nil {
return err
}
if r.Listen == nil {
r.Listen = (*badoption.Addr)(common.Ptr(netip.AddrFrom4([4]byte{127, 0, 0, 53})))
}
if r.ListenPort == 0 {
r.ListenPort = 53
}
return nil
}
type ResolvedDNSServerOptions struct {
Service string `json:"Service"`
AcceptDefaultResolvers bool `json:"accept_default_resolvers,omitempty"`
// NDots int `json:"ndots,omitempty"`
// Timeout badoption.Duration `json:"timeout,omitempty"`
// Attempts int `json:"attempts,omitempty"`
// Rotate bool `json:"rotate,omitempty"`
}

View File

@@ -158,8 +158,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

47
option/service.go Normal file
View File

@@ -0,0 +1,47 @@
package option
import (
"context"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badjson"
"github.com/sagernet/sing/service"
)
type ServiceOptionsRegistry interface {
CreateOptions(serviceType string) (any, bool)
}
type _Service struct {
Type string `json:"type"`
Tag string `json:"tag,omitempty"`
Options any `json:"-"`
}
type Service _Service
func (h *Service) MarshalJSONContext(ctx context.Context) ([]byte, error) {
return badjson.MarshallObjectsContext(ctx, (*_Service)(h), h.Options)
}
func (h *Service) UnmarshalJSONContext(ctx context.Context, content []byte) error {
err := json.UnmarshalContext(ctx, content, (*_Service)(h))
if err != nil {
return err
}
registry := service.FromContext[ServiceOptionsRegistry](ctx)
if registry == nil {
return E.New("missing service fields registry in context")
}
options, loaded := registry.CreateOptions(h.Type)
if !loaded {
return E.New("unknown inbound type: ", h.Type)
}
err = badjson.UnmarshallExcludedContext(ctx, content, (*_Service)(h), options)
if err != nil {
return err
}
h.Options = options
return nil
}

View File

@@ -8,6 +8,7 @@ type ShadowsocksInboundOptions struct {
Users []ShadowsocksUser `json:"users,omitempty"`
Destinations []ShadowsocksDestination `json:"destinations,omitempty"`
Multiplex *InboundMultiplexOptions `json:"multiplex,omitempty"`
Managed bool `json:"managed,omitempty"`
}
type ShadowsocksUser struct {

11
option/ssmapi.go Normal file
View File

@@ -0,0 +1,11 @@
package option
import (
"github.com/sagernet/sing/common/json/badjson"
)
type SSMAPIServiceOptions struct {
ListenOptions
InboundTLSOptionsContainer
Servers *badjson.TypedMap[string, string] `json:"servers"`
}

View File

@@ -2,6 +2,12 @@ package option
import (
"net/netip"
"net/url"
"reflect"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badoption"
M "github.com/sagernet/sing/common/metadata"
)
type TailscaleEndpointOptions struct {
@@ -22,3 +28,59 @@ type TailscaleDNSServerOptions struct {
Endpoint string `json:"endpoint,omitempty"`
AcceptDefaultResolvers bool `json:"accept_default_resolvers,omitempty"`
}
type DERPServiceOptions struct {
ListenOptions
InboundTLSOptionsContainer
ConfigPath string `json:"config_path,omitempty"`
VerifyClientEndpoint badoption.Listable[string] `json:"verify_client_endpoint,omitempty"`
VerifyClientURL badoption.Listable[*DERPVerifyClientURLOptions] `json:"verify_client_url,omitempty"`
MeshWith badoption.Listable[*DERPMeshOptions] `json:"mesh_with,omitempty"`
MeshPSK string `json:"mesh_psk,omitempty"`
MeshPSKFile string `json:"mesh_psk_file,omitempty"`
DomainResolver *DomainResolveOptions `json:"domain_resolver,omitempty"`
}
type _DERPVerifyClientURLOptions struct {
URL string `json:"url,omitempty"`
DialerOptions
}
type DERPVerifyClientURLOptions _DERPVerifyClientURLOptions
func (d DERPVerifyClientURLOptions) ServerIsDomain() bool {
verifyURL, err := url.Parse(d.URL)
if err != nil {
return false
}
return M.IsDomainName(verifyURL.Host)
}
func (d DERPVerifyClientURLOptions) MarshalJSON() ([]byte, error) {
if reflect.DeepEqual(d, _DERPVerifyClientURLOptions{}) {
return json.Marshal(d.URL)
} else {
return json.Marshal(_DERPVerifyClientURLOptions(d))
}
}
func (d *DERPVerifyClientURLOptions) UnmarshalJSON(bytes []byte) error {
var stringValue string
err := json.Unmarshal(bytes, &stringValue)
if err == nil {
d.URL = stringValue
return nil
}
return json.Unmarshal(bytes, (*_DERPVerifyClientURLOptions)(d))
}
type DERPMeshOptions struct {
ServerOptions
Host string `json:"host,omitempty"`
OutboundTLSOptionsContainer
DialerOptions
}
type DERPSTUNServiceOptions struct {
ListenOptions
}

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